diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurencyFormDialogContent.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurencyFormDialogContent.js deleted file mode 100644 index 84e4046ec..000000000 --- a/client/src/containers/Dialogs/CurrencyFormDialog/CurencyFormDialogContent.js +++ /dev/null @@ -1,200 +0,0 @@ -import React, { useMemo, useCallback } from 'react'; -import { - Button, - Classes, - FormGroup, - InputGroup, - Intent, -} from '@blueprintjs/core'; -import * as Yup from 'yup'; -import { useFormik } from 'formik'; -import { useQuery, queryCache } from 'react-query'; -import { FormattedMessage as T, useIntl } from 'react-intl'; -import { pick } from 'lodash'; -import { - ErrorMessage, - AppToaster, - FieldRequiredHint, - DialogContent, -} from 'components'; - -import withDialogActions from 'containers/Dialog/withDialogActions'; -import withCurrencyDetail from 'containers/Currencies/withCurrencyDetail'; -import withCurrenciesActions from 'containers/Currencies/withCurrenciesActions'; - -import { compose } from 'utils'; - -import 'style/pages/Currency/CurrencyFormDialog.scss' - -function CurencyFormDialogContent({ - // #withCurrencyDetail - currency, - - // #wihtCurrenciesActions - requestFetchCurrencies, - requestSubmitCurrencies, - requestEditCurrency, - - // #withDialogActions - closeDialog, - - // #ownProp - action, - currencyId, - dialogName, -}) { - const { formatMessage } = useIntl(); - const fetchCurrencies = useQuery('currencies', () => - requestFetchCurrencies(), - ); - - const validationSchema = Yup.object().shape({ - currency_name: Yup.string() - .required() - .label(formatMessage({ id: 'currency_name_' })), - currency_code: Yup.string() - .max(4) - .required() - .label(formatMessage({ id: 'currency_code_' })), - }); - const initialValues = useMemo( - () => ({ - currency_name: '', - currency_code: '', - }), - [], - ); - const { - values, - errors, - touched, - isSubmitting, - getFieldProps, - handleSubmit, - resetForm, - } = useFormik({ - enableReinitialize: true, - initialValues: { - ...(action === 'edit' && pick(currency, Object.keys(initialValues))), - }, - validationSchema: validationSchema, - onSubmit: (values, { setSubmitting }) => { - if (action === 'edit') { - requestEditCurrency(currency.id, values) - .then((response) => { - closeDialog(dialogName); - AppToaster.show({ - message: formatMessage({ - id: 'the_currency_has_been_edited_successfully', - }), - intent: Intent.SUCCESS, - }); - setSubmitting(false); - queryCache.invalidateQueries('currencies'); - }) - .catch((error) => { - setSubmitting(false); - }); - } else { - requestSubmitCurrencies(values) - .then((response) => { - closeDialog(dialogName); - AppToaster.show({ - message: formatMessage({ - id: 'the_currency_has_been_created_successfully', - }), - intent: Intent.SUCCESS, - }); - setSubmitting(false); - queryCache.invalidateQueries('currencies'); - }) - .catch((error) => { - setSubmitting(false); - }); - } - }, - }); - const handleClose = useCallback(() => { - closeDialog(dialogName); - }, [dialogName, closeDialog]); - - const onDialogOpening = useCallback(() => { - fetchCurrencies.refetch(); - }, [fetchCurrencies]); - - const onDialogClosed = useCallback(() => { - resetForm(); - closeDialog(dialogName); - }, [closeDialog, dialogName, resetForm]); - - return ( - -
-
- } - labelInfo={FieldRequiredHint} - className={'form-group--currency-name'} - intent={ - errors.currency_name && touched.currency_name && Intent.DANGER - } - helperText={ - - } - inline={true} - > - - - - } - labelInfo={FieldRequiredHint} - className={'form-group--currency-code'} - intent={ - errors.currency_code && touched.currency_code && Intent.DANGER - } - helperText={ - - } - inline={true} - > - - -
- -
-
- - -
-
-
-
- ); -} - -export default compose( - withCurrencyDetail, - withDialogActions, - withCurrenciesActions, -)(CurencyFormDialogContent); diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.js new file mode 100644 index 000000000..440802aed --- /dev/null +++ b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.js @@ -0,0 +1,97 @@ +import React, { useMemo, useCallback } from 'react'; +import { Intent } from '@blueprintjs/core'; +import { Formik } from 'formik'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { AppToaster } from 'components'; +import { pick } from 'lodash'; +import CurrencyFormContent from './CurrencyFormContent'; + +import { useCurrencyFormContext } from './CurrencyFormProvider'; +import { + CreateCurrencyFormSchema, + EditCurrencyFormSchema, +} from './CurrencyForm.schema'; +import withDialogActions from 'containers/Dialog/withDialogActions'; + +import { compose, transformToForm } from 'utils'; + +const defaultInitialValues = { + currency_name: '', + currency_code: '', +}; + +/** + * Currency form. + */ +function CurrencyForm({ + // #withDialogActions + closeDialog, +}) { + const { formatMessage } = useIntl(); + + const { + createCurrencyMutate, + editCurrencyMutate, + dialogName, + currency, + isEditMode, + } = useCurrencyFormContext(); + + // Form validation schema in create and edit mode. + const validationSchema = isEditMode + ? EditCurrencyFormSchema + : CreateCurrencyFormSchema; + + const initialValues = useMemo( + () => ({ + ...defaultInitialValues, + // ...(isEditMode && pick(currency, Object.keys(defaultInitialValues))), + ...transformToForm(currency, defaultInitialValues), + }), + [], + ); + + // Handles the form submit. + const handleFormSubmit = (values, { setSubmitting, setErrors }) => { + setSubmitting(true); + + // Handle close the dialog after success response. + const afterSubmit = () => { + closeDialog(dialogName); + }; + + const onSuccess = ({ response }) => { + AppToaster.show({ + message: formatMessage({ + id: isEditMode + ? 'the_currency_has_been_edited_successfully' + : 'the_currency_has_been_created_successfully', + }), + intent: Intent.SUCCESS, + }); + afterSubmit(response); + }; + + // Handle the response error. + const onError = (errors) => { + setSubmitting(false); + }; + if (isEditMode) { + editCurrencyMutate([currency.id, values]).then(onSuccess).catch(onError); + } else { + createCurrencyMutate(values).then(onSuccess).catch(onError); + } + }; + + return ( + + + + ); +} + +export default compose(withDialogActions)(CurrencyForm); diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.schema.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.schema.js new file mode 100644 index 000000000..31c6dfe8b --- /dev/null +++ b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyForm.schema.js @@ -0,0 +1,16 @@ +import * as Yup from 'yup'; +import { formatMessage } from 'services/intl'; +import { DATATYPES_LENGTH } from 'common/dataTypes'; + +const Schema = Yup.object().shape({ + currency_name: Yup.string() + .required() + .label(formatMessage({ id: 'currency_name_' })), + currency_code: Yup.string() + .max(4) + .required() + .label(formatMessage({ id: 'currency_code_' })), +}); + +export const CreateCurrencyFormSchema = Schema; +export const EditCurrencyFormSchema = Schema; diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormContent.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormContent.js new file mode 100644 index 000000000..8b70d2add --- /dev/null +++ b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormContent.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { Form } from 'formik'; + +import CurrencyFormFields from './CurrencyFormFields'; +import CurrencyFormFooter from './CurrencyFormFooter'; + +export default function CurrencyFormContent() { + return ( +
+ + + + ); +} diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormDialogContent.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormDialogContent.js new file mode 100644 index 000000000..439e16a71 --- /dev/null +++ b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormDialogContent.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { CurrencyFormProvider } from './CurrencyFormProvider'; +import { pick } from 'lodash'; + +import CurrencyForm from './CurrencyForm'; +import withCurrencyDetail from 'containers/Currencies/withCurrencyDetail'; + +import { compose } from 'utils'; +import 'style/pages/Currency/CurrencyFormDialog.scss'; + +function CurrencyFormDialogContent({ + // #ownProp + action, + currency, + dialogName, +}) { + return ( + + + + ); +} + +export default compose(withCurrencyDetail)(CurrencyFormDialogContent); diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormFields.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormFields.js new file mode 100644 index 000000000..31f10bed2 --- /dev/null +++ b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormFields.js @@ -0,0 +1,62 @@ +import React, { useMemo, useCallback } from 'react'; +import { + Button, + Classes, + FormGroup, + InputGroup, + Intent, +} from '@blueprintjs/core'; +import { Form, useFormikContext, FastField } from 'formik'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { pick } from 'lodash'; +import { + ErrorMessage, + AppToaster, + FieldRequiredHint, + DialogContent, +} from 'components'; + +import { useAutofocus } from 'hooks'; +import { inputIntent } from 'utils'; + +export default function CurrencyFormFields() { + const currencyNameFieldRef = useAutofocus(); + + return ( +
+ {/* ----------- Currency name ----------- */} + + {({ field, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + className={'form-group--currency-name'} + intent={inputIntent({ error, touched })} + helperText={} + inline={true} + > + (currencyNameFieldRef.current = ref)} + {...field} + /> + + )} + + {/* ----------- Currency Code ----------- */} + + {({ field, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + className={'form-group--currency-code'} + intent={inputIntent({ error, touched })} + helperText={} + inline={true} + > + + + )} + +
+ ); +} diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormFooter.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormFooter.js new file mode 100644 index 000000000..5191bbb5b --- /dev/null +++ b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormFooter.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { useFormikContext } from 'formik'; +import { useCurrencyFormContext } from './CurrencyFormProvider'; + +import { Button, Classes, Intent } from '@blueprintjs/core'; +import { FormattedMessage as T } from 'react-intl'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'utils'; + +function CurrencyFormFooter({ + // #withDialogActions + closeDialog, +}) { + const { isSubmitting } = useFormikContext(); + + const { dialogName, isEditMode } = useCurrencyFormContext(); + + const handleClose = () => { + closeDialog(dialogName); + }; + + return ( +
+
+ + +
+
+ ); +} + +export default compose(withDialogActions)(CurrencyFormFooter); diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormProvider.js b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormProvider.js new file mode 100644 index 000000000..6098b5649 --- /dev/null +++ b/client/src/containers/Dialogs/CurrencyFormDialog/CurrencyFormProvider.js @@ -0,0 +1,38 @@ +import React, { createContext } from 'react'; +import { useCurrencies, useEditCurrency, useCreateCurrency } from 'hooks/query'; +import { DialogContent } from 'components'; + +const CurrencyFormContext = createContext(); + +/** + * Currency Form page provider. + */ + +function CurrencyFormProvider({ isEditMode, currency, dialogName, ...props }) { + // Create and edit item currency mutations. + const { mutateAsync: createCurrencyMutate } = useCreateCurrency(); + const { mutateAsync: editCurrencyMutate } = useEditCurrency(); + + // fetch Currencies list. + const { data: currencies, isFetching: isCurrenciesLoading } = useCurrencies(); + + // Provider state. + const provider = { + createCurrencyMutate, + editCurrencyMutate, + dialogName, + currency, + isCurrenciesLoading, + isEditMode, + }; + + return ( + + + + ); +} + +const useCurrencyFormContext = () => React.useContext(CurrencyFormContext); + +export { CurrencyFormProvider, useCurrencyFormContext }; diff --git a/client/src/containers/Dialogs/CurrencyFormDialog/index.js b/client/src/containers/Dialogs/CurrencyFormDialog/index.js index 19a1a82fc..f97747430 100644 --- a/client/src/containers/Dialogs/CurrencyFormDialog/index.js +++ b/client/src/containers/Dialogs/CurrencyFormDialog/index.js @@ -5,7 +5,7 @@ import withDialogRedux from 'components/DialogReduxConnect'; import { compose } from 'utils'; const CurrencyFormDialogContent = lazy(() => - import('./CurencyFormDialogContent'), + import('./CurrencyFormDialogContent'), ); /** diff --git a/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogContent.js b/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogContent.js index baf2dfd4b..846cfc2d0 100644 --- a/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogContent.js +++ b/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogContent.js @@ -1,101 +1,21 @@ import React from 'react'; -import { Intent } from '@blueprintjs/core'; -import { pick, snakeCase } from 'lodash'; -import { queryCache, useQuery } from 'react-query'; -import { useIntl } from 'react-intl'; -import { Formik } from 'formik'; -import { AppToaster, DialogContent } from 'components'; -import withUsersActions from 'containers/Users/withUsersActions'; -import withDialogActions from 'containers/Dialog/withDialogActions'; +import InviteUserForm from './InviteUserForm'; +import { InviteUserFormProvider } from './InviteUserFormProvider'; -import { compose, objectKeysTransform } from 'utils'; -import { InviteUserFormSchema } from './InviteUserDialog.schema'; -import UserFormDialogForm from './InviteUserDialogForm'; - -import { transformApiErrors } from './utils'; - -import 'style/pages/Users/InviteFormDialog.scss' +import 'style/pages/Users/InviteFormDialog.scss'; /** * Invite user dialog content. */ -function InviteUserDialogContent({ - // #wihtCurrenciesActions - requestFetchUser, - requestSubmitInvite, - - // #withDialogActions - closeDialog, - - // #ownProp +export default function InviteUserDialogContent({ action, userId, dialogName, }) { - const { formatMessage } = useIntl(); - - // Fetch user details. - const fetchHook = useQuery( - ['user', userId], - (key, id) => requestFetchUser(id), - { enabled: userId }, - ); - - const initialValues = { - status: 1, - ...(action === 'edit' && - pick( - objectKeysTransform(userId, snakeCase), - Object.keys(InviteUserFormSchema.fields), - )), - }; - - const handleSubmit = (values, { setSubmitting, setErrors }) => { - 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) => { - const errorsTransformed = transformApiErrors(errors); - - setErrors({ ...errorsTransformed }); - setSubmitting(false); - }); - }; - - const handleCancelBtnClick = () => { - closeDialog('invite-user'); - }; - return ( - - - - - + + + ); } - -export default compose( - // UserFormDialogConnect, - withDialogActions, - withUsersActions, -)(InviteUserDialogContent); diff --git a/client/src/containers/Dialogs/InviteUserDialog/InviteUserForm.js b/client/src/containers/Dialogs/InviteUserDialog/InviteUserForm.js new file mode 100644 index 000000000..7c322db10 --- /dev/null +++ b/client/src/containers/Dialogs/InviteUserDialog/InviteUserForm.js @@ -0,0 +1,77 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { Intent } from '@blueprintjs/core'; +import { pick, snakeCase } from 'lodash'; +import { useIntl } from 'react-intl'; +import { AppToaster } from 'components'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; + +import { InviteUserFormSchema } from './InviteUserDialog.schema'; +import InviteUserFormContent from './InviteUserFormContent'; +import { useInviteUserFormContext } from './InviteUserFormProvider'; + +import { transformApiErrors } from './utils'; + +import { compose, objectKeysTransform } from 'utils'; + +function InviteUserForm({ + // #withDialogActions + closeDialog, +}) { + const { formatMessage } = useIntl(); + + const { + dialogName, + isEditMode, + inviteUserMutate, + userId, + } = useInviteUserFormContext(); + + const initialValues = { + status: 1, + ...(isEditMode && + pick( + objectKeysTransform(userId, snakeCase), + Object.keys(InviteUserFormSchema.fields), + )), + }; + + const handleSubmit = (values, { setSubmitting, setErrors }) => { + const form = { ...values }; + + // Handle close the dialog after success response. + const afterSubmit = () => { + closeDialog(dialogName); + }; + const onSuccess = ({ response }) => { + AppToaster.show({ + message: formatMessage({ + id: 'teammate_invited_to_organization_account', + }), + intent: Intent.SUCCESS, + }); + afterSubmit(response); + }; + + // Handle the response error. + const onError = (errors) => { + const errorsTransformed = transformApiErrors(errors); + + setErrors({ ...errorsTransformed }); + setSubmitting(false); + }; + inviteUserMutate(form).then(onSuccess).catch(onError); + }; + + return ( + + + + ); +} +export default compose(withDialogActions)(InviteUserForm); diff --git a/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogForm.js b/client/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js similarity index 64% rename from client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogForm.js rename to client/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js index a8efbbe1f..79981128c 100644 --- a/client/src/containers/Dialogs/InviteUserDialog/InviteUserDialogForm.js +++ b/client/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js @@ -1,21 +1,24 @@ import React from 'react'; -import { - FormGroup, - InputGroup, - Intent, - Button, -} from '@blueprintjs/core'; +import { FormGroup, InputGroup, Intent, Button } from '@blueprintjs/core'; import { FastField, Form, useFormikContext, ErrorMessage } from 'formik'; import { FormattedMessage as T } from 'react-intl'; import { CLASSES } from 'common/classes'; import classNames from 'classnames'; -import { inputIntent, saveInvoke } from 'utils'; +import { inputIntent } from 'utils'; +import { useInviteUserFormContext } from './InviteUserFormProvider'; -export default function InviteUserDialogForm({ onCancelClick, action }) { +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'utils'; + +function InviteUserFormContent({ + // #withDialogActions + closeDialog, +}) { const { isSubmitting } = useFormikContext(); + const { isEditMode, dialogName } = useInviteUserFormContext(); - const handleCancelBtnClick = (event) => { - saveInvoke(onCancelClick, event); + const handleClose = () => { + closeDialog(dialogName); }; return ( @@ -38,19 +41,21 @@ export default function InviteUserDialogForm({ onCancelClick, action }) { )} - +
-
); } + +export default compose(withDialogActions)(InviteUserFormContent); diff --git a/client/src/containers/Dialogs/InviteUserDialog/InviteUserFormProvider.js b/client/src/containers/Dialogs/InviteUserDialog/InviteUserFormProvider.js new file mode 100644 index 000000000..cd8c92ac7 --- /dev/null +++ b/client/src/containers/Dialogs/InviteUserDialog/InviteUserFormProvider.js @@ -0,0 +1,35 @@ +import React, { createContext } from 'react'; +import { useCreateInviteUser, useUsers } from 'hooks/query'; +import { DialogContent } from 'components'; + +const InviteUserFormContext = createContext(); + +/** + * Invite user Form page provider. + */ +function InviteUserFormProvider({ userId, isEditMode, dialogName, ...props }) { + // Create and edit item currency mutations. + const { mutateAsync: inviteUserMutate } = useCreateInviteUser(); + + // fetch users list. + const { isFetching: isUsersLoading } = useUsers(); + + // Provider state. + const provider = { + inviteUserMutate, + dialogName, + userId, + isUsersLoading, + isEditMode, + }; + + return ( + + + + ); +} + +const useInviteUserFormContext = () => React.useContext(InviteUserFormContext); + +export { InviteUserFormProvider, useInviteUserFormContext }; diff --git a/client/src/hooks/query/currencies.js b/client/src/hooks/query/currencies.js index 47a6e3ef5..4dc2cac46 100644 --- a/client/src/hooks/query/currencies.js +++ b/client/src/hooks/query/currencies.js @@ -25,7 +25,7 @@ export function useCreateCurrency(props) { export function useEditCurrency(props) { const queryClient = useQueryClient(); - return useMutation((currencyCode, values) => + return useMutation(([currencyCode, values]) => ApiService.post(`currencies/${currencyCode}`, values), { onSuccess: () => { diff --git a/client/src/hooks/query/index.js b/client/src/hooks/query/index.js index 937a0539b..640214bfd 100644 --- a/client/src/hooks/query/index.js +++ b/client/src/hooks/query/index.js @@ -16,4 +16,5 @@ export * from './estimates'; export * from './receipts'; export * from './paymentReceives'; export * from './paymentMades'; -export * from './settings'; \ No newline at end of file +export * from './settings'; +export * from './users'; \ No newline at end of file diff --git a/client/src/hooks/query/users.js b/client/src/hooks/query/users.js new file mode 100644 index 000000000..875cdf507 --- /dev/null +++ b/client/src/hooks/query/users.js @@ -0,0 +1,73 @@ +import { useMutation, useQueryClient, useQuery } from 'react-query'; +import { defaultTo } from 'lodash'; +import ApiService from 'services/ApiService'; + +/** + * Create a new invite user. + */ +export function useCreateInviteUser(props) { + const queryClient = useQueryClient(); + return useMutation((values) => ApiService.post('invite/send', values), { + onSuccess: () => { + queryClient.invalidateQueries('USERS'); + }, + ...props, + }); +} + +/** + * Edits the given user. + * + */ +export function useEditUser(props) { + const queryClient = useQueryClient(); + + return useMutation(([id, values]) => ApiService.post(`users/${id}`, values), { + onSuccess: () => { + queryClient.invalidateQueries('USERS'); + }, + ...props, + }); +} + +/** + * Deletes the given user. + */ +export function useDeleteUser(props) { + const queryClient = useQueryClient(); + + return useMutation((id) => ApiService.delete(`users/${id}`), { + onSuccess: () => { + queryClient.invalidateQueries('USERS'); + queryClient.invalidateQueries('USER'); + }, + ...props, + }); +} + +/** + * Retrieves users list. + */ +export function useUsers(props) { + const result = useQuery( + ['USERS'], + () => ApiService.get(`USERS`).then((response) => response.data.users), + props, + ); + + return { + ...result, + data: defaultTo(result.data, {}), + }; +} + +/** + * Retrieve details of the given user. + */ +export function useUser(id, props) { + return useQuery( + ['USER', id], + () => ApiService.get(`users/${id}`).then((response) => response.data.item), + props, + ); +}