diff --git a/client/src/components/DialogsContainer.js b/client/src/components/DialogsContainer.js index a19833c32..69762dd88 100644 --- a/client/src/components/DialogsContainer.js +++ b/client/src/components/DialogsContainer.js @@ -3,10 +3,12 @@ import AccountFormDialog from 'containers/Dashboard/Dialogs/AccountFormDialog'; import UserFormDialog from 'containers/Dashboard/Dialogs/UserFormDialog'; import ItemCategoryDialog from 'containers/Dashboard/Dialogs/ItemCategoryDialog'; import CurrencyDialog from 'containers/Dashboard/Dialogs/CurrencyDialog'; +import InviteUserDialog from 'containers/Dashboard/Dialogs/InviteUserDialog'; export default function DialogsContainer() { return ( + diff --git a/client/src/connectors/UserFormDialog.connector.js b/client/src/connectors/UserFormDialog.connector.js index ea8e8c839..ddd66652f 100644 --- a/client/src/connectors/UserFormDialog.connector.js +++ b/client/src/connectors/UserFormDialog.connector.js @@ -1,12 +1,6 @@ -import {connect} from 'react-redux'; -import { - submitUser, - editUser, - fetchUser, -} from 'store/users/users.actions'; -import { - getUserDetails -} from 'store/users/users.reducer'; +import { connect } from 'react-redux'; +import { submitInvite, editUser, fetchUser } from 'store/users/users.actions'; +import { getUserDetails } from 'store/users/users.reducer'; import { getDialogPayload } from 'store/dashboard/dashboard.reducer'; import t from 'store/types'; @@ -15,18 +9,22 @@ export const mapStateToProps = (state, props) => { return { name: 'user-form', - payload: {action: 'new', id: null}, - userDetails: dialogPayload.action === 'edit' - ? getUserDetails(state, dialogPayload.user.id) : {}, + payload: { action: 'new', id: null }, + userDetails: + dialogPayload.action === 'edit' + ? getUserDetails(state, dialogPayload.user.id) + : {}, }; }; export const mapDispatchToProps = (dispatch) => ({ - openDialog: (name, payload) => dispatch({ type: t.OPEN_DIALOG, name, payload }), - closeDialog: (name, payload) => dispatch({ type: t.CLOSE_DIALOG, name, payload }), - submitUser: (form) => dispatch(submitUser({ form })), - editUser: (id, form) => dispatch(editUser({ form, id })), - fetchUser: (id) => dispatch(fetchUser({ id })), + openDialog: (name, payload) => + dispatch({ type: t.OPEN_DIALOG, name, payload }), + closeDialog: (name, payload) => + dispatch({ type: t.CLOSE_DIALOG, name, payload }), + requestSubmitInvite: (form) => dispatch(submitInvite({ form })), + requestEditUser: (id, form) => dispatch(editUser({ form, id })), + requestFetchUser: (id) => dispatch(fetchUser({ id })), }); -export default connect(mapStateToProps, mapDispatchToProps); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps); diff --git a/client/src/connectors/UsersList.connector.js b/client/src/connectors/UsersList.connector.js index 1513f46f6..3811999a2 100644 --- a/client/src/connectors/UsersList.connector.js +++ b/client/src/connectors/UsersList.connector.js @@ -1,10 +1,32 @@ import { connect } from 'react-redux'; -import { fetchUsers, fetchUser, deleteUser } from 'store/users/users.actions'; +import { + fetchUsers, + fetchUser, + deleteUser, + inactiveUser, + editUser, +} from 'store/users/users.actions'; import t from 'store/types'; +import { getUserDetails } from 'store/users/users.reducer'; +import { getDialogPayload } from 'store/dashboard/dashboard.reducer'; -export const mapStateToProps = (state, props) => ({ - usersList: state.users.list.results, -}); +export const mapStateToProps = (state, props) => { + const dialogPayload = getDialogPayload(state, 'userList-form'); + + return { + name: 'userList-form', + payload: { action: 'new', id: null }, + userDetails: + dialogPayload.action === 'edit' + ? getUserDetails(state, dialogPayload.user.id) + : {}, + editUser: + dialogPayload && dialogPayload.action === 'edit' + ? state.users.list.results[dialogPayload.user.id] + : {}, + usersList: state.users.list.results, + }; +}; export const mapDispatchToProps = (dispatch) => ({ openDialog: (name, payload) => @@ -12,9 +34,11 @@ export const mapDispatchToProps = (dispatch) => ({ closeDialog: (name, payload) => dispatch({ type: t.CLOSE_DIALOG, name, payload }), - fetchUsers: () => dispatch(fetchUsers({})), - fetchUser: (id) => dispatch(fetchUser({ id })), - deleteUser: (id) => dispatch(deleteUser({ id })), + requestFetchUsers: () => dispatch(fetchUsers({})), + requestFetchUser: (id) => dispatch(fetchUser({ id })), + requestDeleteUser: (id) => dispatch(deleteUser({ id })), + requestInactiveUser: (id) => dispatch(inactiveUser({ id })), + requestEditUser: (id, form) => dispatch(editUser({ form, id })), }); export default connect(mapStateToProps, mapDispatchToProps); diff --git a/client/src/containers/Dashboard/Dialogs/InviteUserDialog.js b/client/src/containers/Dashboard/Dialogs/InviteUserDialog.js new file mode 100644 index 000000000..d7161520f --- /dev/null +++ b/client/src/containers/Dashboard/Dialogs/InviteUserDialog.js @@ -0,0 +1,192 @@ +import React, { useMemo } from 'react'; +import { useIntl } from 'react-intl'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; +import { + Dialog, + Button, + FormGroup, + InputGroup, + Intent, + Classes, +} from '@blueprintjs/core'; +import UserListDialogConnect from 'connectors/UsersList.connector'; +import DialogReduxConnect from 'components/DialogReduxConnect'; +import useAsync from 'hooks/async'; +import { objectKeysTransform } from 'utils'; +import { pick, snakeCase } from 'lodash'; +import ErrorMessage from 'components/ErrorMessage'; +import classNames from 'classnames'; +import AppToaster from 'components/AppToaster'; +import { compose } from 'utils'; + +function InviteUserDialog({ + name, + payload, + isOpen, + closeDialog, + requestFetchUser, + requestEditUser, +}) { + const intl = useIntl(); + + const fetchHook = useAsync(async () => { + await Promise.all([ + ...(payload.action === 'edit' ? [requestFetchUser(payload.user.id)] : []), + ]); + }, false); + + const validationSchema = Yup.object().shape({ + first_name: Yup.string().required(intl.formatMessage({ id: 'required' })), + last_name: Yup.string().required(intl.formatMessage({ id: 'required' })), + email: Yup.string() + .email() + .required(intl.formatMessage({ id: 'required' })), + phone_number: Yup.number().required(intl.formatMessage({ id: 'required' })), + }); + + const initialValues = useMemo( + () => ({ + first_name: '', + last_name: '', + email: '', + phone_number: '', + }), + [] + ); + + const formik = useFormik({ + enableReinitialize: true, + initialValues: { + ...(payload.action === 'edit' && + pick( + objectKeysTransform(payload.user, snakeCase), + Object.keys(initialValues) + )), + }, + validationSchema, + onSubmit: (values, { setSubmitting }) => { + const form = { + ...values, + }; + if (payload.action === 'edit') { + requestEditUser(payload.user.id, form) + .then((response) => { + closeDialog(name); + AppToaster.show({ + message: 'the_user_details_has_been_updated', + }); + setSubmitting(false); + }) + .catch((error) => { + setSubmitting(false); + }); + } + }, + }); + const { values, errors, touched } = useMemo(() => formik, [formik]); + + const onDialogOpening = () => { + fetchHook.execute(); + }; + + const onDialogClosed = () => { + formik.resetForm(); + }; + + const handleClose = () => { + closeDialog(name); + }; + + return ( + +
+
+ } + inline={true} + > + + + + } + inline={true} + > + + + + } + inline={true} + > + + + + } + inline={true} + > + + +
+ +
+
+ + +
+
+
+
+ ); +} + +export default compose( + UserListDialogConnect, + DialogReduxConnect +)(InviteUserDialog); diff --git a/client/src/containers/Dashboard/Dialogs/UserFormDialog.js b/client/src/containers/Dashboard/Dialogs/UserFormDialog.js index ec25fb286..eb87af188 100644 --- a/client/src/containers/Dashboard/Dialogs/UserFormDialog.js +++ b/client/src/containers/Dashboard/Dialogs/UserFormDialog.js @@ -21,9 +21,9 @@ import classNames from 'classnames'; import { compose } from 'utils'; function UserFormDialog({ - fetchUser, - submitUser, - editUser, + requestFetchUser, + requestSubmitInvite, + requestEditUser, name, payload, isOpen, @@ -32,12 +32,12 @@ function UserFormDialog({ const intl = useIntl(); const fetchHook = useAsync(async () => { await Promise.all([ - ...(payload.action === 'edit' ? [fetchUser(payload.user.id)] : []), + ...(payload.action === 'edit' ? [requestFetchUser(payload.user.id)] : []), ]); }, false); const validationSchema = Yup.object().shape({ - email: Yup.string().email().required(), + email: Yup.string().email().required(intl.formatMessage({id:'required'})), }); const initialValues = { @@ -47,7 +47,6 @@ function UserFormDialog({ objectKeysTransform(payload.user, snakeCase), Object.keys(validationSchema.fields) )), - password: '', }; const formik = useFormik({ @@ -57,17 +56,16 @@ function UserFormDialog({ onSubmit: (values) => { const form = { ...values, - confirm_password: values.password, }; if (payload.action === 'edit') { - editUser(payload.user.id, form).then((response) => { + requestEditUser(payload.user.id, form).then((response) => { AppToaster.show({ message: 'the_user_details_has_been_updated', }); closeDialog(name); }); } else { - submitUser(form).then((response) => { + requestSubmitInvite(form).then((response) => { AppToaster.show({ message: 'the_user_has_been_invited', }); @@ -93,7 +91,7 @@ function UserFormDialog({ return ( - } - inline={true} - > - -
diff --git a/client/src/containers/Dashboard/Preferences/Users.js b/client/src/containers/Dashboard/Preferences/Users.js index a6fbee459..8fc45ac56 100644 --- a/client/src/containers/Dashboard/Preferences/Users.js +++ b/client/src/containers/Dashboard/Preferences/Users.js @@ -1,49 +1,36 @@ -import React, {useCallback} from 'react'; -import { - Tabs, - Tab, - Button, - Intent, -} from '@blueprintjs/core'; +import React, { useCallback } from 'react'; +import { Tabs, Tab, Button, Intent } from '@blueprintjs/core'; import PreferencesSubContent from 'components/Preferences/PreferencesSubContent'; import connector from 'connectors/UsersPreferences.connector'; -function UsersPreferences({ - openDialog, -}) { - const onChangeTabs = (currentTabId) => { - - }; +function UsersPreferences({ openDialog }) { + const onChangeTabs = (currentTabId) => {}; const onClickNewUser = useCallback(() => { openDialog('user-form'); }, [openDialog]); return ( -
-
- - - - +
+
+ + + -
- +
+ - -
+ +
- +
); } -export default connector(UsersPreferences); \ No newline at end of file +export default connector(UsersPreferences); diff --git a/client/src/containers/Dashboard/Preferences/UsersList.js b/client/src/containers/Dashboard/Preferences/UsersList.js index 5eefd0b1d..5a8512f91 100644 --- a/client/src/containers/Dashboard/Preferences/UsersList.js +++ b/client/src/containers/Dashboard/Preferences/UsersList.js @@ -21,30 +21,48 @@ import DialogConnect from 'connectors/Dialog.connector'; import DashboardConnect from 'connectors/Dashboard.connector'; function UsersListPreferences({ - fetchUsers, + requestFetchUsers, usersList, openDialog, closeDialog, - deleteUser, + requestDeleteUser, + requestInactiveUser, onFetchData, }) { const [deleteUserState, setDeleteUserState] = useState(false); const [inactiveUserState, setInactiveUserState] = useState(false); const asyncHook = useAsync(async () => { - await Promise.all([fetchUsers()]); + await Promise.all([requestFetchUsers()]); }, []); - const onInactiveUser = (user) => {}; + const onInactiveUser = (user) => { + setInactiveUserState(user); + }; + + // Handle cancel inactive user alert + const handleCancelInactiveUser = useCallback(() => { + setInactiveUserState(false); + }, []); + + // handel confirm user activation + const handleConfirmUserActive = useCallback(() => { + requestInactiveUser(inactiveUserState.id).then(() => { + setInactiveUserState(false); + requestFetchUsers(); + AppToaster.show({ message: 'the_user_has_been_inactivated' }); + }); + }, [inactiveUserState, requestInactiveUser, requestFetchUsers]); const onDeleteUser = (user) => { setDeleteUserState(user); }; + const handleCancelUserDelete = () => { setDeleteUserState(false); }; - const onEditUser = (user) => () => { + const onEditInviteUser = (user) => () => { const form = Object.keys(user).reduce((obj, key) => { const camelKey = snakeCase(key); obj[camelKey] = user[key]; @@ -54,12 +72,22 @@ function UsersListPreferences({ openDialog('user-form', { action: 'edit', user: form }); }; + const onEditUser = (user) => () => { + const form = Object.keys(user).reduce((obj, key) => { + const camelKey = snakeCase(key); + obj[camelKey] = user[key]; + return obj; + }, {}); + + openDialog('userList-form', { action: 'edit', user: form }); + }; + const handleConfirmUserDelete = () => { if (!deleteUserState) { return; } - deleteUser(deleteUserState.id).then((response) => { + requestDeleteUser(deleteUserState.id).then((response) => { setDeleteUserState(false); AppToaster.show({ message: 'the_user_has_been_deleted', @@ -67,16 +95,18 @@ function UsersListPreferences({ }); }; - const actionMenuList = (user) => ( - - - - - onInactiveUser(user)} /> - onDeleteUser(user)} /> - + const actionMenuList = useCallback( + (user) => ( + + + + + onInactiveUser(user)} /> + onDeleteUser(user)} /> + + ), + [] ); - console.log(usersList, 'X'); const columns = useMemo( () => [ @@ -151,6 +181,21 @@ function UsersListPreferences({ able to restore it later, but it will become private to you.

+ + +

+ Are you sure you want to move filename to Trash? You will be + able to restore it later, but it will become private to you. +

+
); } diff --git a/client/src/store/users/users.actions.js b/client/src/store/users/users.actions.js index 398bc4b87..a958d65ee 100644 --- a/client/src/store/users/users.actions.js +++ b/client/src/store/users/users.actions.js @@ -1,36 +1,46 @@ -import ApiService from "services/ApiService"; +import ApiService from 'services/ApiService'; import t from 'store/types'; export const fetchUsers = () => { - return (dispatch) => new Promise((resolve, reject) => { - ApiService.get(`users`).then((response) => { - dispatch({ - type: t.USERS_LIST_SET, - users: response.data.users, - }); - resolve(response); - }).catch((error) => { reject(error); }); - }); + return (dispatch) => + new Promise((resolve, reject) => { + ApiService.get(`users`) + .then((response) => { + dispatch({ + type: t.USERS_LIST_SET, + users: response.data.users, + }); + resolve(response); + }) + .catch((error) => { + reject(error); + }); + }); }; export const fetchUser = ({ id }) => { - return (dispatch) => new Promise((resolve, reject) => { - ApiService.get(`users/${id}`).then((response) => { - dispatch({ - type: t.USER_DETAILS_SET, - user: response.data.user, - }); - resolve(response); - }).catch(error => { reject(error); }); - }); + return (dispatch) => + new Promise((resolve, reject) => { + ApiService.get(`users/${id}`) + .then((response) => { + dispatch({ + type: t.USER_DETAILS_SET, + user: response.data.user, + }); + resolve(response); + }) + .catch((error) => { + reject(error); + }); + }); }; export const deleteUser = ({ id }) => { return (dispatch) => ApiService.delete(`users/${id}`); }; -export const submitUser = ({ form }) => { - return (dispatch) => ApiService.post(`users`, form); +export const submitInvite = ({ form }) => { + return (dispatch) => ApiService.post(`invite/send`, form); }; export const editUser = ({ form, id }) => { @@ -38,9 +48,9 @@ export const editUser = ({ form, id }) => { }; export const inactiveUser = ({ id }) => { - return (dispatch) => ApiService.post(`users/${id}/inactive`); + return (dispatch) => ApiService.put(`users/${id}/inactive`); }; export const activeUser = ({ id }) => { - return (dispatch) => ApiService.post(`users/${id}/active`); -} \ No newline at end of file + return (dispatch) => ApiService.put(`users/${id}/active`); +}; diff --git a/client/src/style/App.scss b/client/src/style/App.scss index 478273881..305c88093 100644 --- a/client/src/style/App.scss +++ b/client/src/style/App.scss @@ -47,6 +47,7 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, @import 'pages/items'; @import 'pages/invite-form.scss'; @import "pages/currency"; +@import "pages/invite-user.scss"; // Views @import 'views/filter-dropdown'; diff --git a/client/src/style/pages/currency.scss b/client/src/style/pages/currency.scss index bb1ef27eb..ba6b4d0a2 100644 --- a/client/src/style/pages/currency.scss +++ b/client/src/style/pages/currency.scss @@ -8,5 +8,8 @@ width: 250px; } } + .bp3-dialog-header { + height: 170px; + } } } diff --git a/client/src/style/pages/invite-form.scss b/client/src/style/pages/invite-form.scss index 054bfe527..c391ee877 100644 --- a/client/src/style/pages/invite-form.scss +++ b/client/src/style/pages/invite-form.scss @@ -1,6 +1,3 @@ -// .dialog--invite-form { - -// } .dialog--invite-form { &.bp3-dialog { @@ -30,18 +27,3 @@ } } -// .bp3-dialog-body { -// .bp3-form-group.bp3-inline { -// .bp3-label { -// min-width: 100px; -// } -// .bp3-form-content { -// width: 250px; -// } -// } -// } -// .bp3-dialog-footer-actions { -// display: flex; -// justify-content: flex-end; -// margin-right: 100px; -// } diff --git a/client/src/style/pages/invite-user.scss b/client/src/style/pages/invite-user.scss new file mode 100644 index 000000000..b6b8383c3 --- /dev/null +++ b/client/src/style/pages/invite-user.scss @@ -0,0 +1,12 @@ +.dialog--invite-user { + .bp3-dialog-body { + .bp3-form-group.bp3-inline { + .bp3-label { + min-width: 140px; + } + .bp3-form-content { + width: 250px; + } + } + } +} diff --git a/client/src/style/pages/preferences.scss b/client/src/style/pages/preferences.scss index 39459d673..ec8e98707 100644 --- a/client/src/style/pages/preferences.scss +++ b/client/src/style/pages/preferences.scss @@ -22,7 +22,7 @@ } } &-menu { - width: 240px; + width: 374px; } &-button { margin-right: 10px; @@ -86,7 +86,7 @@ min-width: 140px; } .bp3-form-content { - width: 250px; + width: 384px; } } }