diff --git a/client/src/components/DialogsContainer.js b/client/src/components/DialogsContainer.js index 23ce3cde6..897653a12 100644 --- a/client/src/components/DialogsContainer.js +++ b/client/src/components/DialogsContainer.js @@ -2,9 +2,9 @@ import React, { lazy } from 'react'; import AccountFormDialog from 'containers/Dialogs/AccountFormDialog'; -// import UserFormDialog from 'containers/Dialogs/UserFormDialog'; +import UserFormDialog from 'containers/Dialogs/UserFormDialog'; // import ItemCategoryDialog from 'containers/Dialogs/ItemCategoryDialog'; -// import CurrencyDialog from 'containers/Dialogs/CurrencyDialog'; +import CurrencyFormDialog from 'containers/Dialogs/CurrencyFormDialog'; // import InviteUserDialog from 'containers/Dialogs/InviteUserDialog'; // import ExchangeRateDialog from 'containers/Dialogs/ExchangeRateDialog'; import JournalNumberDialog from 'containers/Dialogs/JournalNumberDialog'; @@ -23,6 +23,8 @@ export default function DialogsContainer() { + + ); } diff --git a/client/src/containers/Currencies/withCurrency.js b/client/src/containers/Currencies/withCurrency.js deleted file mode 100644 index cb38db924..000000000 --- a/client/src/containers/Currencies/withCurrency.js +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from 'react-redux'; -import { - getCurrencyById, - getCurrencyByCode, -} from 'store/currencies/currencies.selector'; - - -const mapStateToProps = (state, props) => ({ - ...(props.currencyId) ? { - currency: getCurrencyById(state.currencies.data, props.currencyId), - } : (props.currencyCode) ? { - currency: getCurrencyByCode(state.currencies.data, props.currencyCode), - } : {}, -}); - -export default connect(mapStateToProps); \ No newline at end of file diff --git a/client/src/containers/Currencies/withCurrencyDetail.js b/client/src/containers/Currencies/withCurrencyDetail.js new file mode 100644 index 000000000..efd0b19a7 --- /dev/null +++ b/client/src/containers/Currencies/withCurrencyDetail.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import { + getCurrencyById, + getCurrencyByCode, +} from 'store/currencies/currencies.selector'; + +const mapStateToProps = (state, props) => ({ + currency: getCurrencyByCode(state, props), +}); + +export default connect(mapStateToProps); + diff --git a/client/src/containers/Dialogs/CurrencyDialog.js b/client/src/containers/Dialogs/CurencyFormDialogContent.js similarity index 75% rename from client/src/containers/Dialogs/CurrencyDialog.js rename to client/src/containers/Dialogs/CurencyFormDialogContent.js index 4aac6789c..9ac0bf890 100644 --- a/client/src/containers/Dialogs/CurrencyDialog.js +++ b/client/src/containers/Dialogs/CurencyFormDialogContent.js @@ -7,40 +7,41 @@ import { Intent, } from '@blueprintjs/core'; import * as Yup from 'yup'; -import { FormattedMessage as T, useIntl } from 'react-intl'; import { useFormik } from 'formik'; import { useQuery, queryCache } from 'react-query'; -import { connect } from 'react-redux'; +import { FormattedMessage as T, useIntl } from 'react-intl'; import { pick } from 'lodash'; - -import AppToaster from 'components/AppToaster'; -import Dialog from 'components/Dialog'; -import withDialogRedux from 'components/DialogReduxConnect'; -import ErrorMessage from 'components/ErrorMessage'; import classNames from 'classnames'; -import withDialogActions from 'containers/Dialog/withDialogActions'; +import { + If, + ErrorMessage, + AppToaster, + FieldRequiredHint, + DialogContent, +} from 'components'; -import withCurrency from 'containers/Currencies/withCurrency'; +import withDialogActions from 'containers/Dialog/withDialogActions'; +import withCurrencyDetail from 'containers/Currencies/withCurrencyDetail'; import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions'; import { compose } from 'utils'; -function CurrencyDialog({ - dialogName, - payload, - isOpen, - - // #withDialogActions - closeDialog, - - // #withCurrency - currencyCode, +function CurencyFormDialogContent({ + // #withCurrencyDetail currency, // #wihtCurrenciesActions requestFetchCurrencies, requestSubmitCurrencies, requestEditCurrency, + + // #withDialogActions + closeDialog, + + // #ownProp + action, + currencyId, + dialogName, }) { const { formatMessage } = useIntl(); const fetchCurrencies = useQuery('currencies', () => @@ -63,7 +64,6 @@ function CurrencyDialog({ }), [], ); - const { values, errors, @@ -75,12 +75,11 @@ function CurrencyDialog({ } = useFormik({ enableReinitialize: true, initialValues: { - ...(payload.action === 'edit' && - pick(currency, Object.keys(initialValues))), + ...(action === 'edit' && pick(currency, Object.keys(initialValues))), }, validationSchema: validationSchema, onSubmit: (values, { setSubmitting }) => { - if (payload.action === 'edit') { + if (action === 'edit') { requestEditCurrency(currency.id, values) .then((response) => { closeDialog(dialogName); @@ -115,7 +114,6 @@ function CurrencyDialog({ } }, }); - const handleClose = useCallback(() => { closeDialog(dialogName); }, [dialogName, closeDialog]); @@ -129,35 +127,13 @@ function CurrencyDialog({ closeDialog(dialogName); }, [closeDialog, dialogName, resetForm]); - const requiredSpan = useMemo(() => *, []); - return ( - - ) : ( - - ) - } - className={classNames( - { - 'dialog--loading': fetchCurrencies.isFetching, - }, - 'dialog--currency-form', - )} - isOpen={isOpen} - onClosed={onDialogClosed} - onOpening={onDialogOpening} - isLoading={fetchCurrencies.isFetching} - onClose={handleClose} - > +
} - labelInfo={requiredSpan} + labelInfo={FieldRequiredHint} className={'form-group--currency-name'} intent={ errors.currency_name && touched.currency_name && Intent.DANGER @@ -178,7 +154,7 @@ function CurrencyDialog({ } - labelInfo={requiredSpan} + labelInfo={FieldRequiredHint} className={'form-group--currency-code'} intent={ errors.currency_code && touched.currency_code && Intent.DANGER @@ -208,29 +184,17 @@ function CurrencyDialog({ type="submit" disabled={isSubmitting} > - {payload.action === 'edit' ? ( - - ) : ( - - )} + {action === 'edit' ? : }
-
+ ); } -const mapStateToProps = (state, props) => ({ - currency: 'currency-form', -}); - -const withCurrencyFormDialog = connect(mapStateToProps); - export default compose( - withCurrencyFormDialog, - withDialogRedux(null, 'currency-form'), - withCurrency, + withCurrencyDetail, withDialogActions, withCurrenciesActions, -)(CurrencyDialog); +)(CurencyFormDialogContent); diff --git a/client/src/containers/Dialogs/CurrencyFormDialog.js b/client/src/containers/Dialogs/CurrencyFormDialog.js new file mode 100644 index 000000000..48c6757cb --- /dev/null +++ b/client/src/containers/Dialogs/CurrencyFormDialog.js @@ -0,0 +1,41 @@ +import React, { lazy } from 'react'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { Dialog, DialogSuspense } from 'components'; +import withDialogRedux from 'components/DialogReduxConnect'; +import { compose } from 'utils'; + +const CurrencyFormDialogContent = lazy(() => + import('./CurencyFormDialogContent'), +); +function CurrencyFormDialog({ + dialogName, + payload = { action: '', id: null }, + isOpen, +}) { + return ( + + ) : ( + + ) + } + className={'dialog--currency-form'} + isOpen={isOpen} + autoFocus={true} + canEscapeKeyClose={true} + > + + + + + ); +} + +export default compose(withDialogRedux())(CurrencyFormDialog); diff --git a/client/src/containers/Dialogs/UserFormDialog.connector.js b/client/src/containers/Dialogs/UserFormDialog.connector.js index 856c81668..6aeb7d2ad 100644 --- a/client/src/containers/Dialogs/UserFormDialog.connector.js +++ b/client/src/containers/Dialogs/UserFormDialog.connector.js @@ -7,7 +7,7 @@ export const mapStateToProps = (state, props) => { const dialogPayload = getDialogPayload(state, 'user-form'); return { - name: 'user-form', + dialogName: 'user-form', payload: { action: 'new', id: null }, userDetails: dialogPayload.action === 'edit' diff --git a/client/src/containers/Dialogs/UserFormDialog.js b/client/src/containers/Dialogs/UserFormDialog.js index accdef28a..09fa56aaf 100644 --- a/client/src/containers/Dialogs/UserFormDialog.js +++ b/client/src/containers/Dialogs/UserFormDialog.js @@ -1,110 +1,19 @@ -import React, { useCallback } from 'react'; +import React, { lazy } from 'react'; import { FormattedMessage as T, useIntl } from 'react-intl'; -import { useFormik } from 'formik'; -import * as Yup from 'yup'; -import { - Dialog, - Button, - FormGroup, - InputGroup, - Intent, - Classes, -} from '@blueprintjs/core'; -import { objectKeysTransform } from 'utils'; -import { pick, snakeCase } from 'lodash'; -import classNames from 'classnames'; -import { queryCache, useQuery } from 'react-query'; - -import AppToaster from 'components/AppToaster'; -import DialogReduxConnect from 'components/DialogReduxConnect'; -import ErrorMessage from 'components/ErrorMessage'; - -import UserFormDialogConnect from 'containers/Dialogs/UserFormDialog.connector'; -import withUsersActions from 'containers/Users/withUsersActions'; -import withDialogActions from 'containers/Dialog/withDialogActions'; - +import { Dialog, DialogSuspense } from 'components'; +import withDialogRedux from 'components/DialogReduxConnect'; import { compose } from 'utils'; +const UserFormDialogContent = lazy(() => import('./UserFormDialogContent')); + function UserFormDialog({ - requestFetchUser, - requestSubmitInvite, - name, - payload, + dialogName, + payload = { action: '', id: null }, isOpen, - closeDialog, }) { - const { formatMessage } = useIntl(); - - const fetchHook = useQuery( - payload.action === 'edit' && ['user', payload.user.id], - (key, id) => requestFetchUser(id), - { manual: true }, - ); - const validationSchema = Yup.object().shape({ - email: Yup.string() - .email() - .required() - .label(formatMessage({ id: 'email' })), - }); - - const initialValues = { - status: 1, - ...(payload.action === 'edit' && - pick( - objectKeysTransform(payload.user, snakeCase), - Object.keys(validationSchema.fields), - )), - }; - - const { - errors, - touched, - resetForm, - getFieldProps, - handleSubmit, - isSubmitting, - } = useFormik({ - enableReinitialize: true, - initialValues, - validationSchema, - onSubmit: (values, { setSubmitting }) => { - const form = { ...values }; - - requestSubmitInvite(form) - .then((response) => { - closeDialog(name); - AppToaster.show({ - message: formatMessage({ - id: 'teammate_invited_to_organization_account', - }), - intent: Intent.SUCCESS, - }); - setSubmitting(false); - queryCache.invalidateQueries('users-table'); - }) - .catch((errors) => { - setSubmitting(false); - }); - }, - }); - - // Handle the dialog opening. - const onDialogOpening = useCallback(() => { - fetchHook.refetch(); - }, [fetchHook]); - - const onDialogClosed = useCallback(() => { - resetForm(); - }, [resetForm]); - - // Handles dialog close. - const handleClose = useCallback(() => { - closeDialog(name); - }, [closeDialog, name]); - return ( @@ -112,66 +21,23 @@ function UserFormDialog({ ) } - className={classNames({ - 'dialog--loading': fetchHook.pending, - 'dialog--invite-form': true, - })} - // + className={'dialog--invite-form'} autoFocus={true} canEscapeKeyClose={true} isOpen={isOpen} - isLoading={fetchHook.pending} - onClosed={onDialogClosed} - onOpening={onDialogOpening} - onClose={handleClose} > -
-
-

- -

- - } - className={classNames('form-group--email', Classes.FILL)} - intent={errors.email && touched.email && Intent.DANGER} - helperText={} - inline={true} - > - - -
- -
-
- - -
-
-
+ + +
); } export default compose( - UserFormDialogConnect, - withUsersActions, - withDialogActions, - DialogReduxConnect, + // UserFormDialogConnect, + withDialogRedux(), )(UserFormDialog); diff --git a/client/src/containers/Dialogs/UserFormDialogContent.js b/client/src/containers/Dialogs/UserFormDialogContent.js new file mode 100644 index 000000000..44bf2ae77 --- /dev/null +++ b/client/src/containers/Dialogs/UserFormDialogContent.js @@ -0,0 +1,162 @@ +import React, { useCallback } from 'react'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; +import { + Button, + FormGroup, + InputGroup, + Intent, + Classes, +} from '@blueprintjs/core'; +import { pick, snakeCase } from 'lodash'; +import classNames from 'classnames'; +import { queryCache, useQuery } from 'react-query'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { + If, + ErrorMessage, + AppToaster, + FieldRequiredHint, + DialogContent, +} from 'components'; + +import UserFormDialogConnect from 'containers/Dialogs/UserFormDialog.connector'; +import withUsersActions from 'containers/Users/withUsersActions'; +import withDialogActions from 'containers/Dialog/withDialogActions'; + +import { compose, objectKeysTransform } from 'utils'; + +function UserFormDialogContent({ + // #wihtCurrenciesActions + requestFetchUser, + requestSubmitInvite, + + // #withDialogActions + closeDialog, + + // #ownProp + action, + userId, + dialogName, +}) { + const { formatMessage } = useIntl(); + + const fetchHook = useQuery( + // action === 'edit' && ['user', action.user.id], + action === 'edit' && ['user', userId], + (key, id) => requestFetchUser(id), + { enabled: userId }, + ); + + const validationSchema = Yup.object().shape({ + email: Yup.string() + .email() + .required() + .label(formatMessage({ id: 'email' })), + }); + + const initialValues = { + status: 1, + ...(action === 'edit' && + pick( + objectKeysTransform(userId, snakeCase), + Object.keys(validationSchema.fields), + )), + }; + + const { + errors, + touched, + resetForm, + getFieldProps, + handleSubmit, + isSubmitting, + } = useFormik({ + enableReinitialize: true, + initialValues, + validationSchema, + onSubmit: (values, { setSubmitting }) => { + const form = { ...values }; + + requestSubmitInvite(form) + .then((response) => { + closeDialog(dialogName); + AppToaster.show({ + message: formatMessage({ + id: 'teammate_invited_to_organization_account', + }), + intent: Intent.SUCCESS, + }); + setSubmitting(false); + queryCache.invalidateQueries('users-table'); + }) + .catch((errors) => { + setSubmitting(false); + }); + }, + }); + + // Handles dialog close. + const handleClose = useCallback(() => { + closeDialog(dialogName); + }, [closeDialog, dialogName]); + + // Handle the dialog opening. + const onDialogOpening = useCallback(() => { + fetchHook.refetch(); + }, [fetchHook]); + + const onDialogClosed = useCallback(() => { + resetForm(); + closeDialog(dialogName); + }, [resetForm]); + + console.log(action, 'action'); + + return ( + +
+
+

+ +

+ + } + className={classNames('form-group--email', Classes.FILL)} + intent={errors.email && touched.email && Intent.DANGER} + helperText={} + inline={true} + > + + +
+ +
+
+ + +
+
+
+
+ ); +} + +export default compose( + // UserFormDialogConnect, + withDialogActions, + withUsersActions, +)(UserFormDialogContent); diff --git a/client/src/containers/Preferences/Currencies/CurrenciesDataTable.js b/client/src/containers/Preferences/Currencies/CurrenciesDataTable.js new file mode 100644 index 000000000..232e6113f --- /dev/null +++ b/client/src/containers/Preferences/Currencies/CurrenciesDataTable.js @@ -0,0 +1,154 @@ +import React, { useCallback, useState, useMemo } from 'react'; +import { + Intent, + Button, + Popover, + Menu, + MenuItem, + Position, +} from '@blueprintjs/core'; +import { withRouter } from 'react-router'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { compose } from 'utils'; +import { useUpdateEffect } from 'hooks'; +import LoadingIndicator from 'components/LoadingIndicator'; +import { DataTable, Icon } from 'components'; + +import withDashboardActions from 'containers/Dashboard/withDashboardActions'; +import withCurrencies from 'containers/Currencies/withCurrencies'; +import withDialogActions from 'containers/Dialog/withDialogActions'; + +function CurrenciesDataTable({ + // #withCurrencies + currenciesList, + currenciesLoading, + + loading, + onFetchData, + onSelectedRowsChange, + onDeleteCurrency, + + // #withDialog. + openDialog, +}) { + const [initialMount, setInitialMount] = useState(false); + + const { formatMessage } = useIntl(); + + useUpdateEffect(() => { + if (!currenciesLoading) { + setInitialMount(true); + } + }, [currenciesLoading, setInitialMount]); + + const handleEditCurrency = useCallback( + (currency) => { + openDialog('currency-form', { + action: 'edit', + currencyCode: currency.currency_code, + }); + }, + [openDialog], + ); + + const actionMenuList = useCallback( + (currency) => ( + + } + text={formatMessage({ id: 'edit_currency' })} + onClick={() => handleEditCurrency(currency)} + /> + + } + text={formatMessage({ id: 'delete_currency' })} + onClick={() => onDeleteCurrency(currency)} + intent={Intent.DANGER} + /> + + ), + [handleEditCurrency, onDeleteCurrency, formatMessage], + ); + + const onRowContextMenu = useCallback( + (cell) => { + return actionMenuList(cell.row.original); + }, + [actionMenuList], + ); + + const columns = useMemo( + () => [ + { + Header: formatMessage({ id: 'currency_name' }), + accessor: 'currency_name', + width: 150, + }, + { + Header: formatMessage({ id: 'currency_code' }), + accessor: 'currency_code', + className: 'currency_code', + width: 120, + }, + { + Header: 'Currency sign', + width: 120, + }, + { + id: 'actions', + Header: '', + Cell: ({ cell }) => ( + + - @@ -512,7 +574,7 @@ function GeneralPreferences({ } export default compose( - withSettings, + withSettings(({ organizationSettings }) => ({ organizationSettings })), withSettingsActions, withDashboardActions, )(GeneralPreferences); diff --git a/client/src/containers/Preferences/Users/UsersDataTable.js b/client/src/containers/Preferences/Users/UsersDataTable.js new file mode 100644 index 000000000..13941d6da --- /dev/null +++ b/client/src/containers/Preferences/Users/UsersDataTable.js @@ -0,0 +1,185 @@ +import React, { useCallback, useState, useMemo } from 'react'; +import { + Intent, + Button, + Popover, + Menu, + MenuDivider, + Tag, + MenuItem, + Position, +} from '@blueprintjs/core'; +import { withRouter } from 'react-router'; +import { snakeCase } from 'lodash'; + +import { + FormattedMessage as T, + FormattedHTMLMessage, + useIntl, +} from 'react-intl'; +import { compose } from 'utils'; +import LoadingIndicator from 'components/LoadingIndicator'; +import { DataTable, Icon, If, AppToaster } from 'components'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; +import withUsers from 'containers/Users/withUsers'; + +function UsersDataTable({ + // #withDialogActions + openDialog, + + // #withUsers + usersList, + usersLoading, + + // #ownProps + loading, + onFetchData, + onInactiveUser, + onDeleteUser, + onSelectedRowsChange, +}) { + const [initialMount, setInitialMount] = useState(false); + const { formatMessage } = useIntl(); + + const onEditUser = useCallback( + (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 }); + }, + [openDialog], + ); + + const actionMenuList = useCallback( + (user) => ( + + + } + text={formatMessage({ id: 'edit_user' })} + onClick={onEditUser(user)} + /> + + + onInactiveUser(user)} + /> + + + } + text={formatMessage({ id: 'delete_user' })} + onClick={() => onDeleteUser(user)} + intent={Intent.DANGER} + /> + + ), + [onInactiveUser, onDeleteUser, onEditUser], + ); + const onRowContextMenu = useCallback( + (cell) => { + return actionMenuList(cell.row.original); + }, + [actionMenuList], + ); + + const columns = useMemo( + () => [ + { + id: 'full_name', + Header: formatMessage({ id: 'full_name' }), + accessor: 'full_name', + width: 150, + }, + { + id: 'email', + Header: formatMessage({ id: 'email' }), + accessor: 'email', + width: 150, + }, + { + id: 'phone_number', + Header: formatMessage({ id: 'phone_number' }), + accessor: 'phone_number', + width: 120, + }, + { + id: 'status', + Header: 'Status', + accessor: (user) => + !user.invite_accepted_at ? ( + + + + ) : user.active ? ( + + + + ) : ( + + + + ), + width: 80, + className: 'status', + }, + { + id: 'actions', + Header: '', + Cell: ({ cell }) => ( + +