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 ( + + +
    + ProfilePic + +
    +

    + {user.get('fullname')} + {user.get('title')} +

    +
    + } + content={ + + } + /> + + ) + }) + + 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 ( + +
    +
    +
    +
    +
    + + + (this.uploadImageInput = r)} + onChange={e => this.onImageUploadChanged(e)} + /> + Profile Picture (this.uploadProfileImage = r)} + /> +
    +
    + this.onFileBtnClick(e)}> + file_upload + +
    +
    +
    +
    +

    + {user.username} + {user.title} +

    +
    +
    +
    +
    +
    this.onSubmitSaveAccount(e)}> +
    +
    + + this.onInputChanged(e, 'name')} + /> +
    +
    + + this.onInputChanged(e, 'title')} + /> +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + this.onInputChanged(e, 'email')} + /> +
    +
    + + this.onRoleSelectChange(e)} + /> +
    +
    + + i._id)} + onChange={e => this.onGroupSelectChange(e)} + ref={r => (this.groupSelect = r)} + /> +
    +
    +
    +
    +
    +
    + ) + } +} + +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'}} -
    -
    - person -
    - - person_add - Create - - {{#canUser data.common.loggedInAccount 'accounts:import'}} - - cloud_upload - Import - - {{/canUser}} -
    -
    -
    - {{/canUser}} - - - - - - - - +
    - - - - - - - - - - - - - -
    -
    -
    -
    - - - - - - - - - - - - -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - -
    -
    -
    - -
    - - -
    - - {{> 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"