diff --git a/package.json b/package.json
index 7d7d0de9a..8b692d90b 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
"prop-types": "15.6.2",
"react": "16.7.0",
"react-dom": "16.7.0",
+ "react-infinite-scroller": "1.2.4",
"react-redux": "6.0.0",
"react-router": "4.3.1",
"react-router-dom": "4.3.1",
diff --git a/src/client/actions/accounts.js b/src/client/actions/accounts.js
new file mode 100644
index 000000000..bee969fc8
--- /dev/null
+++ b/src/client/actions/accounts.js
@@ -0,0 +1,20 @@
+/*
+ * . .o8 oooo
+ * .o8 "888 `888
+ * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo
+ * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P'
+ * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888.
+ * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b.
+ * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o
+ * ========================================================================
+ * Author: Chris Brame
+ * Updated: 2/24/19 2:46 AM
+ * Copyright (c) 2014-2019. All rights reserved.
+ */
+
+import { createAction } from 'redux-actions'
+import { FETCH_ACCOUNTS, SAVE_EDIT_ACCOUNT, UNLOAD_ACCOUNTS } from 'actions/types'
+
+export const fetchAccounts = createAction(FETCH_ACCOUNTS.ACTION, payload => payload, () => ({ thunk: true }))
+export const saveEditAccount = createAction(SAVE_EDIT_ACCOUNT.ACTION)
+export const unloadAccounts = createAction(UNLOAD_ACCOUNTS.ACTION, payload => payload, () => ({ thunk: true }))
diff --git a/src/client/actions/types.js b/src/client/actions/types.js
index b8e61cfc9..30df1da8e 100644
--- a/src/client/actions/types.js
+++ b/src/client/actions/types.js
@@ -40,6 +40,11 @@ export const GET_TAGS_WITH_PAGE = defineAction('GET_TAGS_WITH_PAGE', [SUCCESS, E
export const TAGS_UPDATE_CURRENT_PAGE = defineAction('TAGS_UPDATE_CURRENT_PAGE', [SUCCESS, ERROR])
export const CREATE_TAG = defineAction('CREATE_TAG', [SUCCESS, ERROR])
+// Accounts
+export const FETCH_ACCOUNTS = defineAction('FETCH_ACCOUNTS', [PENDING, SUCCESS, ERROR])
+export const SAVE_EDIT_ACCOUNT = defineAction('SAVE_EDIT_ACCOUNT', [PENDING, SUCCESS, ERROR])
+export const UNLOAD_ACCOUNTS = defineAction('UNLOAD_ACCOUNTS', [SUCCESS])
+
// Settings
export const FETCH_SETTINGS = defineAction('FETCH_SETTINGS', [SUCCESS, ERROR])
export const UPDATE_SETTING = defineAction('UPDATE_SETTING', [SUCCESS, ERROR])
diff --git a/src/client/api/index.js b/src/client/api/index.js
index 043c29c0d..dc5bce975 100644
--- a/src/client/api/index.js
+++ b/src/client/api/index.js
@@ -108,6 +108,22 @@ api.tickets.createTag = ({ name }) => {
})
}
+api.accounts = {}
+api.accounts.getWithPage = payload => {
+ const limit = payload && payload.limit ? payload.limit : 25
+ const page = payload && payload.page ? payload.page : 0
+ let search = payload && payload.search ? payload.search : ''
+ if (search) search = `&search=${search}`
+ return axios.get(`/api/v1/users?limit=${limit}&page=${page}${search}`).then(res => {
+ return res.data
+ })
+}
+api.accounts.updateUser = payload => {
+ return axios.put(`/api/v1/users/${payload.aUsername}`, payload).then(res => {
+ return res.data
+ })
+}
+
api.settings = {}
api.settings.update = settings => {
return axios.put('/api/v1/settings', settings).then(res => {
diff --git a/src/client/components/Drowdown/DropdownItem.jsx b/src/client/components/Drowdown/DropdownItem.jsx
index dbe2d158e..366cc982f 100644
--- a/src/client/components/Drowdown/DropdownItem.jsx
+++ b/src/client/components/Drowdown/DropdownItem.jsx
@@ -16,11 +16,22 @@ import React from 'react'
import PropTypes from 'prop-types'
class DropdownItem extends React.Component {
+ onClick (e) {
+ if (this.props.onClick) {
+ this.props.onClick(e)
+ }
+ }
+
render () {
- const { closeOnClick, text, href } = this.props
+ const { closeOnClick, text, href, extraClass } = this.props
return (
-
+
{text}
@@ -31,6 +42,8 @@ class DropdownItem extends React.Component {
DropdownItem.propTypes = {
href: PropTypes.string,
text: PropTypes.string.isRequired,
+ extraClass: PropTypes.string,
+ onClick: PropTypes.func,
closeOnClick: PropTypes.bool
}
diff --git a/src/client/components/Grid/index.jsx b/src/client/components/Grid/index.jsx
index 3383c3112..3df267d25 100644
--- a/src/client/components/Grid/index.jsx
+++ b/src/client/components/Grid/index.jsx
@@ -19,7 +19,12 @@ class Grid extends React.Component {
render () {
return (
{this.props.children}
@@ -30,8 +35,14 @@ class Grid extends React.Component {
Grid.propTypes = {
extraClass: PropTypes.string,
+ gutterSize: PropTypes.string,
+ collapse: PropTypes.bool,
style: PropTypes.object,
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired
}
+Grid.defaultProps = {
+ collapse: false
+}
+
export default Grid
diff --git a/src/client/components/MultiSelect/index.jsx b/src/client/components/MultiSelect/index.jsx
new file mode 100644
index 000000000..f5036d5b3
--- /dev/null
+++ b/src/client/components/MultiSelect/index.jsx
@@ -0,0 +1,62 @@
+/*
+ * . .o8 oooo
+ * .o8 "888 `888
+ * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo
+ * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P'
+ * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888.
+ * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b.
+ * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o
+ * ========================================================================
+ * Author: Chris Brame
+ * Updated: 2/24/19 2:05 AM
+ * Copyright (c) 2014-2019. All rights reserved.
+ */
+
+import React from 'react'
+import PropTypes from 'prop-types'
+
+import $ from 'jquery'
+import helpers from 'lib/helpers'
+
+class MultiSelect extends React.Component {
+ componentDidMount () {
+ const $select = $(this.select)
+ helpers.UI.multiSelect()
+
+ if (this.props.initialSelected) {
+ $select.multiSelect('select', this.props.initialSelected)
+ $select.multiSelect('refresh')
+ }
+ }
+
+ getSelected () {
+ const $select = $(this.select)
+ if (!$select) return []
+ return $select.val()
+ }
+
+ render () {
+ const { id, items } = this.props
+ return (
+
+ )
+ }
+}
+
+MultiSelect.propTypes = {
+ id: PropTypes.string,
+ items: PropTypes.array.isRequired,
+ initialSelected: PropTypes.array,
+ onChange: PropTypes.func.isRequired
+}
+
+export default MultiSelect
diff --git a/src/client/components/PageContent/index.jsx b/src/client/components/PageContent/index.jsx
new file mode 100644
index 000000000..83445ab37
--- /dev/null
+++ b/src/client/components/PageContent/index.jsx
@@ -0,0 +1,40 @@
+/*
+ * . .o8 oooo
+ * .o8 "888 `888
+ * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo
+ * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P'
+ * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888.
+ * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b.
+ * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o
+ * ========================================================================
+ * Author: Chris Brame
+ * Updated: 2/22/19 11:40 PM
+ * Copyright (c) 2014-2019. All rights reserved.
+ */
+
+import React from 'react'
+import PropTypes from 'prop-types'
+
+import helpers from 'lib/helpers'
+
+class PageContent extends React.Component {
+ componentDidMount () {
+ helpers.resizeFullHeight()
+ helpers.setupScrollers()
+ }
+
+ render () {
+ return (
+
+
{this.props.children}
+
+ )
+ }
+}
+
+PageContent.propTypes = {
+ id: PropTypes.string,
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired
+}
+
+export default PageContent
diff --git a/src/client/components/PageTitle/index.jsx b/src/client/components/PageTitle/index.jsx
new file mode 100644
index 000000000..793fbefed
--- /dev/null
+++ b/src/client/components/PageTitle/index.jsx
@@ -0,0 +1,42 @@
+/*
+ * . .o8 oooo
+ * .o8 "888 `888
+ * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo
+ * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P'
+ * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888.
+ * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b.
+ * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o
+ * ========================================================================
+ * Author: Chris Brame
+ * Updated: 2/22/19 11:32 PM
+ * Copyright (c) 2014-2019. All rights reserved.
+ */
+
+import React from 'react'
+import PropTypes from 'prop-types'
+
+class PageTitle extends React.Component {
+ render () {
+ const { title, rightComponent, shadow } = this.props
+ return (
+
+
+
{title}
+
{rightComponent}
+
+
+ )
+ }
+}
+
+PageTitle.propTypes = {
+ title: PropTypes.string.isRequired,
+ shadow: PropTypes.bool,
+ rightComponent: PropTypes.element
+}
+
+PageTitle.defaultProps = {
+ shadow: false
+}
+
+export default PageTitle
diff --git a/src/client/components/TruCard/index.jsx b/src/client/components/TruCard/index.jsx
new file mode 100644
index 000000000..3534bd055
--- /dev/null
+++ b/src/client/components/TruCard/index.jsx
@@ -0,0 +1,56 @@
+/*
+ * . .o8 oooo
+ * .o8 "888 `888
+ * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo
+ * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P'
+ * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888.
+ * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b.
+ * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o
+ * ========================================================================
+ * Author: Chris Brame
+ * Updated: 2/22/19 11:19 PM
+ * Copyright (c) 2014-2019. All rights reserved.
+ */
+
+import React from 'react'
+import PropTypes from 'prop-types'
+import DropdownTrigger from 'components/Drowdown/DropdownTrigger'
+import Dropdown from 'components/Drowdown'
+
+class TruCard extends React.Component {
+ render () {
+ return (
+
+
+
+ {this.props.menu && (
+
+
+ more_vert
+
+ {this.props.menu.map(child => {
+ return child
+ })}
+
+
+
+ )}
+ {/* HEADER TEXT */}
+
{this.props.header}
+
+ {/* Tru Card Content */}
+
{this.props.content}
+
+
+ )
+ }
+}
+
+TruCard.propTypes = {
+ menu: PropTypes.arrayOf(PropTypes.element),
+ header: PropTypes.element.isRequired,
+ extraHeadClass: PropTypes.string,
+ content: PropTypes.element.isRequired
+}
+
+export default TruCard
diff --git a/src/client/containers/Accounts/index.jsx b/src/client/containers/Accounts/index.jsx
new file mode 100644
index 000000000..c75dc9274
--- /dev/null
+++ b/src/client/containers/Accounts/index.jsx
@@ -0,0 +1,209 @@
+/*
+ * . .o8 oooo
+ * .o8 "888 `888
+ * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo
+ * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P'
+ * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888.
+ * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b.
+ * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o
+ * ========================================================================
+ * Author: Chris Brame
+ * Updated: 2/22/19 11:18 PM
+ * Copyright (c) 2014-2019. All rights reserved.
+ */
+
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { observer } from 'mobx-react'
+import { observable } from 'mobx'
+import axios from 'axios'
+import Log from '../../logger'
+
+import { showModal } from 'actions/common'
+import { fetchAccounts, unloadAccounts } from 'actions/accounts'
+
+import TruCard from 'components/TruCard'
+import PageTitle from 'components/PageTitle'
+import Grid from 'components/Grid'
+import GridItem from 'components/Grid/GridItem'
+import PageContent from 'components/PageContent'
+import DropdownItem from 'components/Drowdown/DropdownItem'
+import InfiniteScroll from 'react-infinite-scroller'
+
+import helpers from 'lib/helpers'
+
+@observer
+class AccountsContainer extends React.Component {
+ @observable initialLoad = true
+ @observable hasMore = true
+ @observable pageStart = -1
+
+ constructor (props) {
+ super(props)
+
+ this.getUsersWithPage = this.getUsersWithPage.bind(this)
+ }
+
+ componentDidMount () {
+ this.initialLoad = false
+ }
+
+ componentDidUpdate () {
+ helpers.resizeFullHeight()
+ }
+
+ componentWillUnmount () {
+ this.props.unloadAccounts()
+ }
+
+ onEditAccountClicked (e, user) {
+ e.preventDefault(e)
+ this.props.showModal('EDIT_ACCOUNT', {
+ user: user.toJS(),
+ roles: this.props.common.roles,
+ groups: this.props.common.groups
+ })
+ }
+
+ getUsersWithPage (page) {
+ this.props.fetchAccounts({ page, limit: 25 }).then(({ response, payload }) => {
+ if (response.count < 25) this.hasMore = false
+ })
+ }
+
+ onSearchKeyUp (e) {
+ const keyCode = e.keyCode || e.which
+ const search = e.target.value
+ if (keyCode === 13) {
+ if (search.length > 2) {
+ this.props.unloadAccounts().then(() => {
+ this.props.fetchAccounts({ limit: 1000, search: search }).then(({ response }) => {
+ this.pageStart = -1
+ if (response.count < 25) this.hasMore = false
+ })
+ })
+ } else if (search.length === 0) {
+ this.props.unloadAccounts().then(() => {
+ this.pageStart = -1
+ this.getUsersWithPage(0)
+ })
+ }
+ }
+ }
+
+ render () {
+ const items = this.props.accountsState.accounts.map(user => {
+ const userImage = user.get('image') || 'defaultProfile.jpg'
+ let actionMenu = [
this.onEditAccountClicked(e, user)} />]
+ if (user.get('deleted')) actionMenu.push()
+ else actionMenu.push()
+ const isAdmin = user.getIn(['role', 'isAdmin']) || false
+ const isAgent = user.getIn(['role', 'isAgent']) || false
+ return (
+
+
+
+
+
+
+
+ {user.get('fullname')}
+ {user.get('title')}
+
+
+ }
+ content={
+
+ -
+
+ Role
+ {user.getIn(['role', 'name'])}
+
+
+ -
+
+
+ -
+
+ Groups
+
+ {user.get('groups').map(group => {
+ return group.get('name') + (user.get('groups').length > 1 ? ', ' : '')
+ })}
+
+
+
+
+ }
+ />
+
+ )
+ })
+
+ return (
+
+
+
+
+
this.onSearchKeyUp(e)} />
+
+
+
+ }
+ />
+
+
+
+
+ }
+ useWindow={false}
+ getScrollParent={() => document.getElementById('accounts-page-content')}
+ >
+ {items}
+
+
+
+ )
+ }
+}
+
+AccountsContainer.propTypes = {
+ fetchAccounts: PropTypes.func.isRequired,
+ unloadAccounts: PropTypes.func.isRequired,
+ showModal: PropTypes.func.isRequired,
+ common: PropTypes.object.isRequired,
+ accountsState: PropTypes.object.isRequired
+}
+
+const mapStateToProps = state => ({
+ accountsState: state.accountsState,
+ common: state.common
+})
+
+export default connect(
+ mapStateToProps,
+ { fetchAccounts, unloadAccounts, showModal }
+)(AccountsContainer)
diff --git a/src/client/containers/Modals/BaseModal.jsx b/src/client/containers/Modals/BaseModal.jsx
index 6fc07c258..5e9279ee9 100644
--- a/src/client/containers/Modals/BaseModal.jsx
+++ b/src/client/containers/Modals/BaseModal.jsx
@@ -55,8 +55,19 @@ class BaseModal extends React.Component {
render () {
return (
- (this.modal = i)} data-modal-tag={this.props.modalTag}>
-
+
(this.modal = i)}
+ data-modal-tag={this.props.modalTag}
+ >
+
{this.props.children}
@@ -70,6 +81,8 @@ BaseModal.propTypes = {
modalTag: PropTypes.string,
hideModal: PropTypes.func.isRequired,
clearModal: PropTypes.func.isRequired,
+ parentExtraClass: PropTypes.string,
+ extraClass: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired
}
diff --git a/src/client/containers/Modals/EditAccountModal.jsx b/src/client/containers/Modals/EditAccountModal.jsx
new file mode 100644
index 000000000..251f1a498
--- /dev/null
+++ b/src/client/containers/Modals/EditAccountModal.jsx
@@ -0,0 +1,233 @@
+/*
+ * . .o8 oooo
+ * .o8 "888 `888
+ * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo
+ * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P'
+ * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888.
+ * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b.
+ * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o
+ * ========================================================================
+ * Author: Chris Brame
+ * Updated: 2/23/19 4:03 PM
+ * Copyright (c) 2014-2019. All rights reserved.
+ */
+
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { observer } from 'mobx-react'
+import { observable } from 'mobx'
+import axios from 'axios'
+import Log from '../../logger'
+
+import { saveEditAccount } from 'actions/accounts'
+
+import Button from 'components/Button'
+import BaseModal from 'containers/Modals/BaseModal'
+import SingleSelect from 'components/SingleSelect'
+import MultiSelect from 'components/MultiSelect'
+
+import helpers from 'lib/helpers'
+
+@observer
+class EditAccountModal extends React.Component {
+ @observable name = ''
+ @observable title = ''
+ @observable password = ''
+ @observable confirmPassword = ''
+ @observable email = ''
+
+ selectedRole = ''
+
+ componentDidMount () {
+ this.name = this.props.user.fullname
+ this.title = this.props.user.title
+ this.email = this.props.user.email
+
+ helpers.UI.inputs()
+ helpers.UI.reRenderInputs()
+ }
+
+ componentDidUpdate () {
+ helpers.UI.reRenderInputs()
+ }
+
+ onFileBtnClick (e) {
+ e.stopPropagation()
+ if (this.uploadImageInput) this.uploadImageInput.click()
+ }
+
+ onImageUploadChanged (e) {
+ const self = e.target
+ const that = this
+ let formData = new FormData()
+ formData.append('username', this.props.user.username)
+ formData.append('_id', this.props.user._id)
+ formData.append('image', self.files[0])
+
+ axios
+ .post('/accounts/uploadImage', formData)
+ .then(res => {
+ const timestamp = new Date().getTime()
+ that.uploadProfileImage.setAttribute('src', `${res.data}?${timestamp}`)
+ })
+ .catch(err => {
+ Log.error(err)
+ })
+ }
+
+ onInputChanged (e, stateName) {
+ this[stateName] = e.target.value
+ }
+
+ onRoleSelectChange (e) {
+ this.selectedRole = e.target.value
+ }
+
+ onSubmitSaveAccount (e) {
+ e.preventDefault()
+ const data = {
+ aUsername: this.props.user.username,
+ aFullname: this.name,
+ aTitle: this.title,
+ aEmail: this.email,
+ aGrps: this.groupSelect.getSelected(),
+ saveGroups: true,
+ aRole: this.selectedRole,
+ aPass: this.password.length > 1 ? this.password : undefined,
+ aPassConfirm: this.confirmPassword.length > 1 ? this.confirmPassword : undefined
+ }
+
+ this.props.saveEditAccount(data)
+ }
+
+ render () {
+ const { user } = this.props
+ const profilePicture = user.image || 'defaultProfile.jpg'
+ const roles = this.props.common.roles.map(role => {
+ return { text: role.name, value: role._id }
+ })
+ const groups = this.props.common.groups.map(group => {
+ return { text: group.name, value: group._id }
+ })
+ return (
+
+
+
+
+
+
+ {user.username}
+ {user.title}
+
+
+
+
+
+
+ )
+ }
+}
+
+EditAccountModal.propTypes = {
+ user: PropTypes.object.isRequired,
+ common: PropTypes.object.isRequired,
+ saveEditAccount: PropTypes.func.isRequired
+}
+
+const mapStateToProps = state => ({
+ common: state.common
+})
+
+export default connect(
+ mapStateToProps,
+ { saveEditAccount }
+)(EditAccountModal)
diff --git a/src/client/containers/Modals/index.jsx b/src/client/containers/Modals/index.jsx
index b088601c8..607aa06ed 100644
--- a/src/client/containers/Modals/index.jsx
+++ b/src/client/containers/Modals/index.jsx
@@ -28,6 +28,7 @@ import CreateTicketModal from './CreateTicketModal'
import CreateRoleModal from './CreateRoleModal'
import DeleteRoleModal from './DeleteRoleModal'
import ViewAllNotificationsModal from './ViewAllNotificationsModal'
+import EditAccountModal from './EditAccountModal'
const MODAL_COMPONENTS = {
NOTICE_ALERT: NoticeAlertModal,
@@ -40,7 +41,8 @@ const MODAL_COMPONENTS = {
CREATE_TAG: CreateTagModal,
CREATE_ROLE: CreateRoleModal,
DELETE_ROLE: DeleteRoleModal,
- VIEW_ALL_NOTIFICATIONS: ViewAllNotificationsModal
+ VIEW_ALL_NOTIFICATIONS: ViewAllNotificationsModal,
+ EDIT_ACCOUNT: EditAccountModal
}
const ModalRoot = ({ modalType, modalProps }) => {
diff --git a/src/client/reducers/accountsReducer.js b/src/client/reducers/accountsReducer.js
new file mode 100644
index 000000000..2c7073631
--- /dev/null
+++ b/src/client/reducers/accountsReducer.js
@@ -0,0 +1,58 @@
+/*
+ * . .o8 oooo
+ * .o8 "888 `888
+ * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo
+ * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P'
+ * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888.
+ * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b.
+ * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o
+ * ========================================================================
+ * Author: Chris Brame
+ * Updated: 2/24/19 7:39 PM
+ * Copyright (c) 2014-2019. All rights reserved.
+ */
+
+import { fromJS, Map, List } from 'immutable'
+import produce from 'immer'
+import { handleActions } from 'redux-actions'
+import { FETCH_ACCOUNTS, SAVE_EDIT_ACCOUNT, UNLOAD_ACCOUNTS } from 'actions/types'
+
+const initialState = {
+ accounts: List([])
+}
+
+const reducer = handleActions(
+ {
+ [FETCH_ACCOUNTS.SUCCESS]: (state, action) => {
+ let arr = state.accounts.toArray()
+ action.payload.response.users.map(i => {
+ arr.push(i)
+ })
+ return {
+ ...state,
+ accounts: fromJS(arr)
+ }
+ },
+
+ [SAVE_EDIT_ACCOUNT.SUCCESS]: (state, action) => {
+ const resUser = action.response.user
+ const accountIndex = state.accounts.findIndex(u => {
+ return u.get('_id') === resUser._id
+ })
+ return {
+ ...state,
+ accounts: state.accounts.set(accountIndex, fromJS(resUser))
+ }
+ },
+
+ [UNLOAD_ACCOUNTS.SUCCESS]: state => {
+ return {
+ ...state,
+ accounts: state.accounts.clear()
+ }
+ }
+ },
+ initialState
+)
+
+export default reducer
diff --git a/src/client/reducers/index.js b/src/client/reducers/index.js
index e1d9efba4..895e2afba 100644
--- a/src/client/reducers/index.js
+++ b/src/client/reducers/index.js
@@ -21,12 +21,14 @@ import modal from './shared/modalReducer'
import sidebar from './sidebarReducer'
import settings from './settings'
import tagsSettings from './tagsReducer'
+import accountsState from './accountsReducer'
const IndexReducer = combineReducers({
shared,
common,
modal,
sidebar,
+ accountsState,
settings,
tagsSettings,
form
diff --git a/src/client/renderer.jsx b/src/client/renderer.jsx
index 1e0e84f74..1776d1016 100644
--- a/src/client/renderer.jsx
+++ b/src/client/renderer.jsx
@@ -16,9 +16,20 @@ import { Provider } from 'react-redux'
import ReactDOM from 'react-dom'
import React from 'react'
-import SettingsContainer from './containers/Settings/SettingsContainer'
+import SettingsContainer from 'containers/Settings/SettingsContainer'
+import AccountsContainer from 'containers/Accounts'
export default function (store) {
+ if (document.getElementById('accounts-container')) {
+ const AccountsContainerWithProvider = (
+
+
+
+ )
+
+ ReactDOM.render(AccountsContainerWithProvider, document.getElementById('accounts-container'))
+ }
+
if (document.getElementById('settings-container')) {
const SettingsContainerWithProvider = (
diff --git a/src/client/sagas/accounts/index.js b/src/client/sagas/accounts/index.js
new file mode 100644
index 000000000..fdf66597d
--- /dev/null
+++ b/src/client/sagas/accounts/index.js
@@ -0,0 +1,63 @@
+/*
+ * . .o8 oooo
+ * .o8 "888 `888
+ * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo
+ * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P'
+ * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888.
+ * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b.
+ * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o
+ * ========================================================================
+ * Author: Chris Brame
+ * Updated: 2/24/19 2:48 AM
+ * Copyright (c) 2014-2019. All rights reserved.
+ */
+
+import { call, put, takeLatest, takeEvery } from 'redux-saga/effects'
+import { FETCH_ACCOUNTS, HIDE_MODAL, SAVE_EDIT_ACCOUNT, UNLOAD_ACCOUNTS } from 'actions/types'
+
+import Log from '../../logger'
+
+import api from '../../api'
+
+import helpers from 'lib/helpers'
+
+function * fetchAccounts ({ payload, meta }) {
+ try {
+ const response = yield call(api.accounts.getWithPage, payload)
+ yield put({ type: FETCH_ACCOUNTS.SUCCESS, payload: { response, payload }, meta })
+ } catch (error) {
+ let errorText = ''
+ if (error.response) errorText = error.response.data.error
+ helpers.UI.showSnackbar(`Error: ${errorText}`, true)
+ Log.error(errorText, error.response || error)
+ yield put({ type: FETCH_ACCOUNTS.ERROR, error })
+ }
+}
+
+function * saveEditAccount ({ payload }) {
+ try {
+ const response = yield call(api.accounts.updateUser, payload)
+ yield put({ type: SAVE_EDIT_ACCOUNT.SUCCESS, response })
+ yield put({ type: HIDE_MODAL.ACTION })
+ } catch (error) {
+ let errorText = ''
+ if (error.response) errorText = error.response.data.error
+ helpers.UI.showSnackbar(`Error: ${errorText}`, true)
+ Log.error(errorText, error.response || error)
+ yield put({ type: SAVE_EDIT_ACCOUNT.ERROR, error })
+ }
+}
+
+function * unloadThunk ({ payload, meta }) {
+ try {
+ yield put({ type: UNLOAD_ACCOUNTS.SUCCESS, payload, meta })
+ } catch (error) {
+ Log.error(error)
+ }
+}
+
+export default function * watcher () {
+ yield takeLatest(FETCH_ACCOUNTS.ACTION, fetchAccounts)
+ yield takeLatest(SAVE_EDIT_ACCOUNT.ACTION, saveEditAccount)
+ yield takeEvery(UNLOAD_ACCOUNTS.ACTION, unloadThunk)
+}
diff --git a/src/client/sagas/index.js b/src/client/sagas/index.js
index da7e08863..77a7f99f5 100644
--- a/src/client/sagas/index.js
+++ b/src/client/sagas/index.js
@@ -16,7 +16,8 @@ import { all } from 'redux-saga/effects'
import CommonSaga from './common'
import SettingsSaga from './settings'
import TicketSaga from './tickets'
+import AccountSaga from './accounts'
export default function * IndexSagas () {
- yield all([CommonSaga(), TicketSaga(), SettingsSaga()])
+ yield all([CommonSaga(), TicketSaga(), SettingsSaga(), AccountSaga()])
}
diff --git a/src/controllers/accounts.js b/src/controllers/accounts.js
index 1962a3a48..9ae771d86 100644
--- a/src/controllers/accounts.js
+++ b/src/controllers/accounts.js
@@ -483,6 +483,7 @@ accountsController.uploadImage = function (req, res) {
})
busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
+ console.log(file)
if (mimetype.indexOf('image/') === -1) {
error = {
status: 400,
diff --git a/src/controllers/api/v1/users.js b/src/controllers/api/v1/users.js
index 73937ad61..e9c8d7c2a 100644
--- a/src/controllers/api/v1/users.js
+++ b/src/controllers/api/v1/users.js
@@ -87,7 +87,9 @@ apiUsers.getWithLimit = function (req, res) {
})
})
- user.groups = _.map(groups, 'name')
+ user.groups = _.map(groups, function (group) {
+ return { name: group.name, _id: group._id }
+ })
result.push(stripUserFields(user))
return c()
@@ -334,9 +336,12 @@ apiUsers.createPublicAccount = function (req, res) {
*/
apiUsers.update = function (req, res) {
var username = req.params.username
+ if (_.isNull(username) || _.isUndefined(username))
+ return res.status(400).json({ success: false, error: 'Invalid Post Data' })
+
var data = req.body
// saveGroups - Profile saving where groups are not sent
- var saveGroups = data.saveGroups
+ var saveGroups = data.saveGroups || true
var obj = {
fullname: data.aFullname,
title: data.aTitle,
@@ -358,6 +363,7 @@ apiUsers.update = function (req, res) {
user: function (done) {
UserSchema.getUserByUsername(username, function (err, user) {
if (err) return done(err)
+ if (!user) return done('Invalid User Object')
obj._id = user._id
@@ -380,28 +386,36 @@ apiUsers.update = function (req, res) {
user.save(function (err, nUser) {
if (err) return done(err)
- var resUser = stripUserFields(nUser)
+ nUser.populate('role', function (err, populatedUser) {
+ if (err) return done(err)
+ var resUser = stripUserFields(populatedUser)
- done(null, resUser)
+ return done(null, resUser)
+ })
})
})
},
groups: function (done) {
if (!saveGroups) return done()
+ var userGroups = []
groupSchema.getAllGroups(function (err, groups) {
if (err) return done(err)
async.each(
groups,
function (grp, callback) {
if (_.includes(obj.groups, grp._id.toString())) {
- if (grp.isMember(obj._id)) return callback()
+ if (grp.isMember(obj._id)) {
+ userGroups.push(grp)
+ return callback()
+ }
grp.addMember(obj._id, function (err, result) {
if (err) return callback(err)
if (result) {
grp.save(function (err) {
if (err) return callback(err)
- callback()
+ userGroups.push(grp)
+ return callback()
})
} else {
return callback()
@@ -415,7 +429,7 @@ apiUsers.update = function (req, res) {
grp.save(function (err) {
if (err) return callback(err)
- callback()
+ return callback()
})
} else {
return callback()
@@ -426,7 +440,7 @@ apiUsers.update = function (req, res) {
function (err) {
if (err) return done(err)
- done()
+ return done(null, userGroups)
}
)
})
@@ -438,7 +452,12 @@ apiUsers.update = function (req, res) {
return res.status(400).json({ success: false, error: err })
}
- return res.json({ success: true, user: results.user })
+ var user = results.user.toJSON()
+ user.groups = results.groups.map(function (g) {
+ return { _id: g._id, name: g.name }
+ })
+
+ return res.json({ success: true, user: user })
}
)
}
@@ -1055,7 +1074,6 @@ function stripUserFields (user) {
user.password = undefined
user.accessToken = undefined
user.__v = undefined
- // user.role = undefined;
user.tOTPKey = undefined
user.iOSDeviceTokens = undefined
diff --git a/src/models/ticket.js b/src/models/ticket.js
index cf74f9666..f97fc0fbc 100644
--- a/src/models/ticket.js
+++ b/src/models/ticket.js
@@ -52,7 +52,7 @@ var COLLECTION = 'tickets'
* @property {Boolean} deleted ```Required``` [default: false] If they ticket is flagged as deleted.
* @property {TicketType} type ```Required``` Reference to the TicketType
* @property {Number} status ```Required``` [default: 0] Ticket Status. (See {@link Ticket#setStatus})
- * @property {Number} prioirty ```Required```
+ * @property {Number} priority ```Required```
* @property {Array} tags An array of Tags.
* @property {String} subject ```Required``` The subject of the ticket. (Overview)
* @property {String} issue ```Required``` Detailed information about the ticket problem/task
diff --git a/src/public/js/modules/ajaxify.js b/src/public/js/modules/ajaxify.js
index eca42ab6a..73da10578 100644
--- a/src/public/js/modules/ajaxify.js
+++ b/src/public/js/modules/ajaxify.js
@@ -230,6 +230,9 @@ define('modules/ajaxify', [
// This will be removed once angular and ajaxy are gone (react-router will Replace)
if (document.getElementById('settings-container'))
window.react.dom.unmountComponentAtNode(document.getElementById('settings-container'))
+ if (document.getElementById('accounts-container'))
+ window.react.dom.unmountComponentAtNode(document.getElementById('accounts-container'))
+
// if (document.getElementById('modal-wrapper'))
// window.react.dom.unmountComponentAtNode(document.getElementById('modal-wrapper'))
diff --git a/src/sass/_settings.sass b/src/sass/_settings.sass
index 5202df8a4..c57c7d5af 100644
--- a/src/sass/_settings.sass
+++ b/src/sass/_settings.sass
@@ -41,8 +41,10 @@ $header_primary: #f6f7f8 !default
$accent_success: #29b955 !default
// Highlight color for actions and Errors
$accent_danger: #d32f2f !default
-
+// Highlight color for warnings
$accent_warn: #feca57 !default
+// Highlight color for agents
+$accent_agent: #7d5fff
// ------------------------------------------------------------------------
diff --git a/src/sass/partials/common.sass b/src/sass/partials/common.sass
index cc04bb77b..042866a5f 100644
--- a/src/sass/partials/common.sass
+++ b/src/sass/partials/common.sass
@@ -97,19 +97,37 @@
margin-top: 10px !important
.mt-20
margin-top: 20px !important
-.mb-10
- margin-bottom: 10px !important
.mb-5
margin-bottom: 5px !important
+.mb-10
+ margin-bottom: 10px !important
+.mb-15
+ margin-bottom: 15px !important
+.mb-25
+ margin-bottom: 25px !important
.mr-10
margin-right: 10px !important
.mr-15
margin-right: 15px !important
+.p-0
+ padding: 0 !important
+.pt-0
+ padding-top: 0 !important
.pr-10
padding-right: 10px !important
+.pr-20
+ padding-right: 20px !important
+.pr-25
+ padding-right: 25px !important
.pl-0
padding-left: 0 !important
+.pl-25
+ padding-left: 25px !important
+.pb-25
+ padding-bottom: 25px !important
+.pb-100
+ padding-bottom: 100px !important
.padding-left-right-15
padding-left: 15px !important
@@ -120,6 +138,9 @@
padding: 10px !important
.padding-15
padding: 15px !important
+.p-25,
+.padding-25
+ padding: 25px !important
.padtop20
padding-top: 20px !important
.padright20
diff --git a/src/sass/partials/trucard.sass b/src/sass/partials/trucard.sass
index 6a6ecf223..156e4c7d9 100644
--- a/src/sass/partials/trucard.sass
+++ b/src/sass/partials/trucard.sass
@@ -26,12 +26,16 @@
position: relative
border-bottom: 1px solid rgba(0,0,0,.12)
&.tru-card-head-admin,
+ &.tru-card-head-agent,
&.tru-card-head-deleted
- background: #3498db
+ background: $accent_blue
+ border-bottom: none
.tru-card-head-text
color: white
.tru-icon
color: white !important
+ &.tru-card-head-agent
+ background: $accent_agent
&.tru-card-head-deleted
background: $accent_danger !important
.tru-card-head-menu
diff --git a/src/views/accounts.hbs b/src/views/accounts.hbs
index 62950da06..65d78a907 100644
--- a/src/views/accounts.hbs
+++ b/src/views/accounts.hbs
@@ -1,164 +1,6 @@
-
-
{{data.page}}
-
-
-
Accounts
-
- {{#canUser data.common.loggedInAccount 'accounts:create'}}
-
- {{/canUser}}
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{#each data.accounts}}
-
-
-
-
-
-
- {{#if image}}
-
- {{else}}
-
- {{/if}}
-
-
-
-
-
- {{fullname}}
- {{firstCap title}}
-
-
-
-
- -
-
- Role
- {{firstCap role.name}}
-
-
- -
-
-
- -
-
- Groups
-
- {{#each groups}}
- {{this}}{{#compare (size ../groups) '>' 1}}, {{/compare}}
- {{/each}}
-
-
-
-
-
-
-
- {{/each}}
-
-
-
-
-
-
-
-
-
- {{> createAccountWindow}}
- {{> editAccountWindow}}
-
{{#contentFor 'js-plugins'}}
{{/contentFor}}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index ab4d62435..35008e598 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6204,6 +6204,11 @@ immediate@~3.0.5:
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
+immer@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-2.0.0.tgz#d834c874d2d249f73511af39f3ef1ed3b7411132"
+ integrity sha512-qwwvbGbidU0P5SjO4s1wZ9hjNj8fQ908UVtKDSAYgxuDiY1MFylCvsQJSr/fUUo3aeHdxZapdpzPi3vpwTUXUQ==
+
immutable@4.0.0-rc.12:
version "4.0.0-rc.12"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0-rc.12.tgz#ca59a7e4c19ae8d9bf74a97bdf0f6e2f2a5d0217"
@@ -10351,6 +10356,13 @@ react-dom@16.7.0:
prop-types "^15.6.2"
scheduler "^0.12.0"
+react-infinite-scroller@1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.4.tgz#f67eaec4940a4ce6417bebdd6e3433bfc38826e9"
+ integrity sha512-/oOa0QhZjXPqaD6sictN2edFMsd3kkMiE19Vcz5JDgHpzEJVqYcmq+V3mkwO88087kvKGe1URNksHEOt839Ubw==
+ dependencies:
+ prop-types "^15.5.8"
+
react-is@^16.6.3, react-is@^16.7.0:
version "16.7.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa"