refactoring: migrating to react-query to manage service-side state.

This commit is contained in:
a.bouhuolia
2021-02-07 08:10:21 +02:00
parent e093be0663
commit adac2386bb
284 changed files with 8255 additions and 6610 deletions

View File

@@ -3,24 +3,29 @@ import { Intent } from '@blueprintjs/core';
import { Formik } from 'formik';
import { useIntl } from 'react-intl';
import { omit } from 'lodash';
import { useQuery, queryCache } from 'react-query';
import { AppToaster, DialogContent } from 'components';
import AccountFormDialogFields from './AccountFormDialogFields';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withAccountDetail from 'containers/Accounts/withAccountDetail';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
EditAccountFormSchema,
CreateAccountFormSchema,
} from './AccountForm.schema';
import {
useAccounts,
useAccountsTypes,
useCreateAccount,
useAccount,
useEditAccount
} from 'hooks/query';
import { compose, transformToForm } from 'utils';
import { transformApiErrors, transformAccountToForm } from './utils';
import 'style/pages/Accounts/AccountFormDialog.scss';
// Default initial form values.
const defaultInitialValues = {
account_type: '',
parent_account_id: '',
@@ -34,16 +39,7 @@ const defaultInitialValues = {
* Account form dialog content.
*/
function AccountFormDialogContent({
// #withAccountDetail
account,
// #withAccountsActions
requestFetchAccounts,
requestFetchAccountTypes,
requestFetchAccount,
requestSubmitAccount,
requestEditAccount,
// #withDialogActions
closeDialog,
// #ownProp
@@ -61,6 +57,27 @@ function AccountFormDialogContent({
? CreateAccountFormSchema
: EditAccountFormSchema;
const { mutateAsync: createAccountMutate } = useCreateAccount();
const { mutateAsync: editAccountMutate } = useEditAccount();
// Fetches accounts list.
const {
data: accounts,
isLoading: isAccountsLoading,
} = useAccounts();
// Fetches accounts types.
const {
data: accountsTypes,
isLoading: isAccountsTypesLoading
} = useAccountsTypes();
// Fetches the specific account details.
const {
data: account,
isLoading: isAccountLoading,
} = useAccount(accountId, { enabled: !!accountId });
// Callbacks handles form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = omit(values, ['subaccount']);
@@ -71,11 +88,6 @@ function AccountFormDialogContent({
// Handle request success.
const handleSuccess = () => {
closeDialog(dialogName);
queryCache.invalidateQueries('accounts-table');
setTimeout(() => {
queryCache.invalidateQueries('accounts-list');
}, 1000);
AppToaster.show({
message: formatMessage(
@@ -94,16 +106,16 @@ function AccountFormDialogContent({
};
// Handle request error.
const handleError = (errors) => {
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
// const errorsTransformed = transformApiErrors(errors);
// setErrors({ ...errorsTransformed });
// setSubmitting(false);
};
if (accountId) {
requestEditAccount(accountId, form)
editAccountMutate(accountId, form)
.then(handleSuccess)
.catch(handleError);
} else {
requestSubmitAccount({ form }).then(handleSuccess).catch(handleError);
createAccountMutate({ ...form }).then(handleSuccess).catch(handleError);
}
};
@@ -130,27 +142,10 @@ function AccountFormDialogContent({
closeDialog(dialogName);
}, [closeDialog, dialogName]);
// Fetches accounts list.
const fetchAccountsList = useQuery('accounts-list', () =>
requestFetchAccounts(),
);
// Fetches accounts types.
const fetchAccountsTypes = useQuery('accounts-types-list', () =>
requestFetchAccountTypes(),
);
// Fetch the given account id on edit mode.
const fetchAccount = useQuery(
['account', accountId],
(key, _id) => requestFetchAccount(_id),
{ enabled: accountId },
);
const isFetching =
fetchAccountsList.isFetching ||
fetchAccountsTypes.isFetching ||
fetchAccount.isFetching;
isAccountsLoading ||
isAccountsTypesLoading ||
isAccountLoading;
return (
<DialogContent isLoading={isFetching}>
@@ -160,6 +155,8 @@ function AccountFormDialogContent({
onSubmit={handleFormSubmit}
>
<AccountFormDialogFields
accounts={accounts}
accountsTypes={accountsTypes}
dialogName={dialogName}
action={action}
onClose={handleClose}
@@ -170,7 +167,5 @@ function AccountFormDialogContent({
}
export default compose(
withAccountsActions,
withAccountDetail,
withDialogActions,
)(AccountFormDialogContent);

View File

@@ -28,11 +28,9 @@ import { useAutofocus } from 'hooks';
* Account form dialogs fields.
*/
function AccountFormDialogFields({
// #ownPropscl
// #ownProps
onClose,
action,
// #withAccounts
accounts,
accountsTypes,
}) {

View File

@@ -4,10 +4,16 @@ import { FormGroup, InputGroup } from '@blueprintjs/core';
import { inputIntent } from 'utils';
import { Row, Col, MoneyInputGroup } from 'components';
import { FormattedMessage as T } from 'react-intl';
import { useAutofocus } from 'hooks';
import { decrementQuantity } from './utils';
import { toSafeNumber } from 'utils';
/**
* Decrement adjustment fields.
*/
function DecrementAdjustmentFields() {
const decrementFieldRef = useAutofocus();
return (
<Row className={'row--decrement-fields'}>
{/*------------ Quantity on hand -----------*/}
@@ -47,6 +53,7 @@ function DecrementAdjustmentFields() {
value={field.value}
allowDecimals={false}
allowNegativeValue={true}
inputRef={(ref) => (decrementFieldRef.current = ref)}
onChange={(value) => {
setFieldValue('quantity', value);
}}

View File

@@ -1,12 +1,15 @@
import React from 'react';
import { Field, FastField, ErrorMessage } from 'formik';
import { FormGroup, InputGroup } from '@blueprintjs/core';
import { useAutofocus } from 'hooks';
import { Row, Col, MoneyInputGroup } from 'components';
import { inputIntent, toSafeNumber } from 'utils';
import { FormattedMessage as T } from 'react-intl';
import { decrementQuantity, incrementQuantity } from './utils';
function IncrementAdjustmentFields() {
export default function IncrementAdjustmentFields() {
const incrementFieldRef = useAutofocus();
return (
<Row>
{/*------------ Quantity on hand -----------*/}
@@ -47,6 +50,7 @@ function IncrementAdjustmentFields() {
value={field.value}
allowDecimals={false}
allowNegativeValue={true}
inputRef={(ref) => (incrementFieldRef.current = ref)}
onChange={(value) => {
setFieldValue('quantity', value);
}}
@@ -131,5 +135,3 @@ function IncrementAdjustmentFields() {
</Row>
);
}
export default IncrementAdjustmentFields;

View File

@@ -2,29 +2,45 @@ import React from 'react';
import { Intent, Button, Classes } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { saveInvoke } from 'utils';
export default function InventoryAdjustmentFloatingActions({
onCloseClick,
onSubmitClick,
import { useInventoryAdjContext } from './InventoryAdjustmentFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Inventory adjustment floating actions.
*/
function InventoryAdjustmentFloatingActions({
// #withDialogActions
closeDialog,
}) {
const { isSubmitting } = useFormikContext();
// Formik context.
const { isSubmitting, submitForm } = useFormikContext();
// Inventory adjustment dialog context.
const { dialogName, setSubmitPayload } = useInventoryAdjContext();
// handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
publish: false,
});
setSubmitPayload({ publish: false });
submitForm();
};
// Handle submit make adjustment button click.
const handleSubmitMakeAdjustmentBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
publish: true,
});
setSubmitPayload({ publish: true });
submitForm();
};
// Handle close button click.
const handleCloseBtnClick = (event) => {
closeDialog(dialogName)
};
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={onCloseClick} style={{ minWidth: '75px' }}>
<Button onClick={handleCloseBtnClick} style={{ minWidth: '75px' }}>
<T id={'close'} />
</Button>
@@ -49,3 +65,7 @@ export default function InventoryAdjustmentFloatingActions({
</div>
);
}
export default compose(
withDialogActions
)(InventoryAdjustmentFloatingActions);

View File

@@ -0,0 +1,90 @@
import React from 'react';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
import { Formik } from 'formik';
import { omit, get } from 'lodash';
import { useIntl } from 'react-intl';
import 'style/pages/Items/ItemAdjustmentDialog.scss';
import { AppToaster } from 'components';
import { CreateInventoryAdjustmentFormSchema } from './InventoryAdjustmentForm.schema';
import InventoryAdjustmentFormContent from './InventoryAdjustmentFormContent';
import { useInventoryAdjContext } from './InventoryAdjustmentFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const defaultInitialValues = {
date: moment(new Date()).format('YYYY-MM-DD'),
type: 'decrement',
adjustment_account_id: '',
item_id: '',
reason: '',
cost: '',
quantity: '',
reference_no: '',
quantity_on_hand: '',
publish: '',
};
/**
* Inventory adjustment form.
*/
function InventoryAdjustmentForm({
// #withDialogActions
closeDialog,
}) {
const {
dialogName,
item,
itemId,
submitPayload,
createInventoryAdjMutate,
} = useInventoryAdjContext();
const { formatMessage } = useIntl();
// Initial form values.
const initialValues = {
...defaultInitialValues,
item_id: itemId,
quantity_on_hand: get(item, 'quantity_on_hand', 0),
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = {
...omit(values, ['quantity_on_hand', 'new_quantity', 'action']),
publish: submitPayload.publish,
};
setSubmitting(true);
createInventoryAdjMutate(form)
.then(() => {
closeDialog(dialogName);
AppToaster.show({
message: formatMessage({
id: 'the_make_adjustment_has_been_created_successfully',
}),
intent: Intent.SUCCESS,
});
})
.finally(() => {
setSubmitting(true);
});
};
return (
<Formik
validationSchema={CreateInventoryAdjustmentFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<InventoryAdjustmentFormContent />
</Formik>
);
}
export default compose(withDialogActions)(InventoryAdjustmentForm);

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { Form } from 'formik';
import InventoryAdjustmentFormDialogFields from './InventoryAdjustmentFormDialogFields';
import InventoryAdjustmentFloatingActions from './InventoryAdjustmentFloatingActions';
/**
* Inventory adjustment form content.
*/
export default function InventoryAdjustmentFormContent() {
return (
<Form>
<InventoryAdjustmentFormDialogFields />
<InventoryAdjustmentFloatingActions />
</Form>
);
}

View File

@@ -1,142 +1,21 @@
import React, { useState, useCallback } from 'react';
import { Intent } from '@blueprintjs/core';
import { Formik, Form } from 'formik';
import { useIntl } from 'react-intl';
import { useQuery, queryCache } from 'react-query';
import moment from 'moment';
import { omit, get } from 'lodash';
import React from 'react';
import 'style/pages/Items/ItemAdjustmentDialog.scss';
import { AppToaster, DialogContent } from 'components';
import { CreateInventoryAdjustmentFormSchema } from './InventoryAdjustmentForm.schema';
import InventoryAdjustmentFormDialogFields from './InventoryAdjustmentFormDialogFields';
import InventoryAdjustmentFloatingActions from './InventoryAdjustmentFloatingActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withInventoryAdjustmentActions from 'containers/Items/withInventoryAdjustmentActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withItem from 'containers/Items/withItem';
import withItemsActions from 'containers/Items/withItemsActions';
import { compose } from 'utils';
const defaultInitialValues = {
date: moment(new Date()).format('YYYY-MM-DD'),
type: 'decrement',
adjustment_account_id: '',
item_id: '',
reason: '',
cost: '',
quantity: '',
reference_no: '',
quantity_on_hand: '',
publish: '',
};
import { InventoryAdjustmentFormProvider } from './InventoryAdjustmentFormProvider';
import InventoryAdjustmentForm from './InventoryAdjustmentForm';
/**
* Inventory adjustment form dialog content.
*/
function InventoryAdjustmentFormDialogContent({
// #withDialogActions
closeDialog,
// #withAccountsActions
requestFetchAccounts,
// #withInventoryAdjustmentActions
requestSubmitInventoryAdjustment,
// #withItemsActions
requestFetchItem,
// #withItem
item,
// #ownProp
itemId,
export default function InventoryAdjustmentFormDialogContent({
// #ownProps
dialogName,
itemId
}) {
const { formatMessage } = useIntl();
const [submitPayload, setSubmitPayload] = useState({});
// Fetches accounts list.
const fetchAccount = useQuery('accounts-list', () => requestFetchAccounts());
// Fetches the item details.
const fetchItem = useQuery(['item', itemId],
(key, id) => requestFetchItem(id));
// Initial form values.
const initialValues = {
...defaultInitialValues,
item_id: itemId,
quantity_on_hand: get(item, 'quantity_on_hand', 0),
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = {
...omit(values, ['quantity_on_hand', 'new_quantity', 'action']),
publish: submitPayload.publish,
};
const onSuccess = ({ response }) => {
closeDialog(dialogName);
queryCache.invalidateQueries('accounts-list');
queryCache.invalidateQueries('items-table');
AppToaster.show({
message: formatMessage({
id: 'the_make_adjustment_has_been_created_successfully',
}),
intent: Intent.SUCCESS,
});
};
const onError = (error) => {
setSubmitting(false);
};
requestSubmitInventoryAdjustment({ form }).then(onSuccess).catch(onError);
};
// Handles dialog close.
const handleCloseClick = useCallback(() => {
closeDialog(dialogName);
}, [closeDialog, dialogName]);
const handleSubmitClick = useCallback(
(event, payload) => {
setSubmitPayload({ ...payload });
},
[setSubmitPayload],
);
return (
<DialogContent isLoading={fetchAccount.isFetching || fetchItem.isFetching}>
<Formik
validationSchema={CreateInventoryAdjustmentFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<InventoryAdjustmentFormDialogFields dialogName={dialogName} />
<InventoryAdjustmentFloatingActions
onSubmitClick={handleSubmitClick}
onCloseClick={handleCloseClick}
/>
</Form>
</Formik>
</DialogContent>
<InventoryAdjustmentFormProvider itemId={itemId} dialogName={dialogName}>
<InventoryAdjustmentForm />
</InventoryAdjustmentFormProvider>
);
}
export default compose(
withInventoryAdjustmentActions,
withDialogActions,
withAccountsActions,
withItem(({ item }) => ({
item: item
})),
withItemsActions,
)(InventoryAdjustmentFormDialogContent);

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { FastField, ErrorMessage, Field, useFormikContext } from 'formik';
import { FastField, ErrorMessage, Field } from 'formik';
import {
Classes,
FormGroup,
@@ -10,7 +10,7 @@ import {
import classNames from 'classnames';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { DateInput } from '@blueprintjs/datetime';
import { compose } from 'redux';
import { useAutofocus } from 'hooks';
import { ListSelect, FieldRequiredHint, Col, Row } from 'components';
import {
inputIntent,
@@ -23,18 +23,20 @@ import { CLASSES } from 'common/classes';
import adjustmentType from 'common/adjustmentType';
import AccountsSuggestField from 'components/AccountsSuggestField';
import withAccounts from 'containers/Accounts/withAccounts';
import { useInventoryAdjContext } from './InventoryAdjustmentFormProvider'
import { diffQuantity } from './utils';
import InventoryAdjustmentQuantityFields from './InventoryAdjustmentQuantityFields';
/**
* Inventory adjustment form dialogs fields.
*/
function InventoryAdjustmentFormDialogFields({
//# withAccount
accountsList,
}) {
const { values } = useFormikContext();
export default function InventoryAdjustmentFormDialogFields() {
const dateFieldRef = useAutofocus();
// Inventory adjustment dialog context.
const { accounts } = useInventoryAdjContext();
// Intl context.
const { formatMessage } = useIntl();
return (
@@ -62,6 +64,7 @@ function InventoryAdjustmentFormDialogFields({
position: Position.BOTTOM,
minimal: true,
}}
inputRef={(ref) => (dateFieldRef.current = ref)}
/>
</FormGroup>
)}
@@ -115,7 +118,7 @@ function InventoryAdjustmentFormDialogFields({
className={'form-group--adjustment-account'}
>
<AccountsSuggestField
accounts={accountsList}
accounts={accounts}
onAccountSelected={(item) =>
form.setFieldValue('adjustment_account_id', item.id)
}
@@ -159,9 +162,3 @@ function InventoryAdjustmentFormDialogFields({
</div>
);
}
export default compose(
withAccounts(({ accountsList }) => ({
accountsList,
})),
)(InventoryAdjustmentFormDialogFields);

View File

@@ -0,0 +1,51 @@
import React, { useState, createContext } from 'react';
import { DialogContent } from 'components';
import {
useItem,
useAccounts,
useCreateInventoryAdjustment,
} from 'hooks/query';
const InventoryAdjustmentContext = createContext();
/**
* Inventory adjustment dialog provider.
*/
function InventoryAdjustmentFormProvider({ itemId, dialogName, ...props }) {
// Fetches accounts list.
const { isFetching: isAccountsLoading, data: accounts } = useAccounts();
// Fetches the item details.
const { isFetching: isItemLoading, data: item } = useItem(itemId);
const {
mutateAsync: createInventoryAdjMutate,
} = useCreateInventoryAdjustment();
// Submit payload.
const [submitPayload, setSubmitPayload] = useState({});
// State provider.
const provider = {
itemId,
isAccountsLoading,
accounts,
isItemLoading,
item,
submitPayload,
dialogName,
createInventoryAdjMutate,
setSubmitPayload,
};
return (
<DialogContent isLoading={isAccountsLoading || isItemLoading}>
<InventoryAdjustmentContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useInventoryAdjContext = () => React.useContext(InventoryAdjustmentContext);
export { InventoryAdjustmentFormProvider, useInventoryAdjContext };

View File

@@ -1,165 +1,111 @@
import React, { useCallback } from 'react';
import {
Button,
Classes,
FormGroup,
InputGroup,
Intent,
TextArea,
MenuItem,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { ErrorMessage, Form, FastField } from 'formik';
import {
ListSelect,
AccountsSelectList,
FieldRequiredHint,
Hint,
} from 'components';
import { inputIntent } from 'utils';
import React, { useMemo } from 'react';
import { useIntl } from 'react-intl';
import { Intent } from '@blueprintjs/core';
import { Formik } from 'formik';
import { useAutofocus } from 'hooks';
import { AppToaster } from 'components';
import { useItemCategoryContext } from './ItemCategoryProvider';
import { transformToForm } from 'utils';
import {
CreateItemCategoryFormSchema,
EditItemCategoryFormSchema,
} from './ItemCategoryForm.schema';
export default function ItemCategoryForm({
itemCategoryId,
accountsList,
categoriesList,
isSubmitting,
onClose,
import withDialogActions from 'containers/Dialog/withDialogActions';
import ItemCategoryFormContent from './ItemCategoryFormContent'
import { compose } from 'utils';
const defaultInitialValues = {
name: '',
description: '',
cost_account_id: '',
sell_account_id: '',
inventory_account_id: '',
};
/**
* Item category form.
*/
function ItemCategoryForm({
// #withDialogActions
closeDialog,
}) {
const categoryNameFieldRef = useAutofocus();
const { formatMessage } = useIntl();
const {
isNewMode,
itemCategory,
itemCategoryId,
dialogName,
createItemCategoryMutate,
editItemCategoryMutate,
} = useItemCategoryContext();
// Initial values.
const initialValues = useMemo(
() => ({
...defaultInitialValues,
...transformToForm(itemCategory, defaultInitialValues),
}),
[itemCategory],
);
// Transformes response errors.
const transformErrors = (errors, { setErrors }) => {
if (errors.find((error) => error.type === 'CATEGORY_NAME_EXISTS')) {
setErrors({
name: formatMessage({ id: 'category_name_exists' }),
});
}
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
const form = { ...values };
// Handle close the dialog after success response.
const afterSubmit = () => {
closeDialog(dialogName);
};
// Handle the response success/
const onSuccess = ({ response }) => {
AppToaster.show({
message: formatMessage({
id: isNewMode
? 'the_item_category_has_been_created_successfully'
: 'the_item_category_has_been_edited_successfully',
}),
intent: Intent.SUCCESS,
});
afterSubmit(response);
};
// Handle the response error.
const onError = (errors) => {
transformErrors(errors, { setErrors });
setSubmitting(false);
};
if (isNewMode) {
createItemCategoryMutate(form).then(onSuccess).catch(onError);
} else {
editItemCategoryMutate([itemCategoryId, form])
.then(onSuccess)
.catch(onError);
}
};
return (
<Form>
<div className={Classes.DIALOG_BODY}>
{/* ----------- Category name ----------- */}
<FastField name={'name'}>
{({ field, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'category_name'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--category-name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="name" />}
inline={true}
>
<InputGroup
medium={true}
inputRef={(ref) => (categoryNameFieldRef.current = ref)}
{...field}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Description ----------- */}
<FastField name={'description'}>
{({ field, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="description" />}
inline={true}
>
<TextArea growVertically={true} large={true} {...field} />
</FormGroup>
)}
</FastField>
{/* ----------- Cost account ----------- */}
<FastField name={'cost_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'cost_account'} />}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="cost_account_id" />}
className={classNames(
'form-group--cost-account',
'form-group--select-list',
Classes.FILL,
)}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={(account) => {
form.setFieldValue('cost_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
filterByTypes={['cost_of_goods_sold']}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Sell account ----------- */}
<FastField name={'sell_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'sell_account'} />}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="sell_account_id" />}
className={classNames(
'form-group--sell-account',
'form-group--select-list',
Classes.FILL,
)}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={(account) => {
form.setFieldValue('sell_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
filterByTypes={['income']}
/>
</FormGroup>
)}
</FastField>
{/* ----------- inventory account ----------- */}
<FastField name={'inventory_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'inventory_account'} />}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="inventory_account_id" />}
className={classNames(
'form-group--sell-account',
'form-group--select-list',
Classes.FILL,
)}
>
<AccountsSelectList
accounts={accountsList}
onAccountSelected={(account) => {
form.setFieldValue('inventory_account_id', account.id);
}}
defaultSelectText={<T id={'select_account'} />}
selectedAccountId={value}
/>
</FormGroup>
)}
</FastField>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={onClose}>
<T id={'close'} />
</Button>
<Button intent={Intent.PRIMARY} type="submit" disabled={isSubmitting}>
{itemCategoryId ? <T id={'edit'} /> : <T id={'submit'} />}
</Button>
</div>
</div>
</Form>
<Formik
validationSchema={
isNewMode ? CreateItemCategoryFormSchema : EditItemCategoryFormSchema
}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<ItemCategoryFormContent />
</Formik>
);
}
export default compose(
withDialogActions,
)(ItemCategoryForm);

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { Form } from 'formik';
import ItemCategoryFormFields from './ItemCategoryFormFields';
import ItemCategoryFormFooter from './ItemCategoryFormFooter';
export default function ItemCategoryForm() {
return (
<Form>
<ItemCategoryFormFields />
<ItemCategoryFormFooter />
</Form>
);
}

View File

@@ -1,168 +1,23 @@
import React, { useMemo, useCallback } from 'react';
import { Intent } from '@blueprintjs/core';
import { useQuery, queryCache } from 'react-query';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Formik } from 'formik';
import { AppToaster, DialogContent } from 'components';
import React from 'react';
import { ItemCategoryProvider } from './ItemCategoryProvider';
import ItemCategoryForm from './ItemCategoryForm';
import withItemCategories from 'containers/Items/withItemCategories';
import withItemCategoryDetail from 'containers/Items/withItemCategoryDetail';
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
import withAccounts from 'containers/Accounts/withAccounts';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
EditItemCategoryFormSchema,
CreateItemCategoryFormSchema,
} from './itemCategoryForm.schema';
import { compose, transformToForm } from 'utils';
import 'style/pages/ItemCategory/ItemCategoryDialog.scss'
const defaultInitialValues = {
name: '',
description: '',
cost_account_id: '',
sell_account_id: '',
inventory_account_id: '',
};
import 'style/pages/ItemCategory/ItemCategoryDialog.scss';
/**
* Item Category form dialog content.
*/
function ItemCategoryFormDialogContent({
// #withDialogActions
closeDialog,
// #withItemCategoryDetail
itemCategoryDetail,
// #withItemCategories
categoriesList,
// #withItemCategoriesActions
requestSubmitItemCategory,
requestEditItemCategory,
requestFetchItemCategories,
//# withAccount
accountsList,
// #withAccountsActions
requestFetchAccounts,
export default function ItemCategoryFormDialogContent({
// #ownProp
action,
itemCategoryId,
dialogName,
}) {
const isNewMode = !itemCategoryId;
const { formatMessage } = useIntl();
// Fetches categories list.
const fetchCategoriesList = useQuery(['items-categories-list'], () =>
requestFetchItemCategories(),
);
// Fetches accounts list.
const fetchAccountsList = useQuery('accounts-list', () =>
requestFetchAccounts(),
);
const initialValues = useMemo(
() => ({
...defaultInitialValues,
...transformToForm(itemCategoryDetail, defaultInitialValues),
}),
[],
);
const transformErrors = (errors, { setErrors }) => {
if (errors.find((error) => error.type === 'CATEGORY_NAME_EXISTS')) {
setErrors({
name: formatMessage({ id: 'category_name_exists' }),
});
}
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
const form = { ...values };
const afterSubmit = () => {
closeDialog(dialogName);
queryCache.invalidateQueries('items-categories-list');
queryCache.invalidateQueries('accounts-list');
};
const onSuccess = ({ response }) => {
AppToaster.show({
message: formatMessage({
id: isNewMode
? 'the_item_category_has_been_created_successfully'
: 'the_item_category_has_been_edited_successfully',
}),
intent: Intent.SUCCESS,
});
afterSubmit(response);
};
const onError = (errors) => {
transformErrors(errors, { setErrors });
setSubmitting(false);
};
if (isNewMode) {
requestSubmitItemCategory(form).then(onSuccess).catch(onError);
} else {
requestEditItemCategory(itemCategoryId, form)
.then(onSuccess)
.catch(onError);
}
};
// Handles dialog close.
const handleClose = useCallback(() => {
closeDialog(dialogName);
}, [closeDialog, dialogName]);
return (
<DialogContent
isLoading={fetchCategoriesList.isFetching || fetchAccountsList.isFetching}
<ItemCategoryProvider
itemCategoryId={itemCategoryId}
dialogName={dialogName}
>
<Formik
validationSchema={
isNewMode ? CreateItemCategoryFormSchema : EditItemCategoryFormSchema
}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
{({ isSubmitting }) => (
<ItemCategoryForm
itemCategoryId={itemCategoryId}
accountsList={accountsList}
categoriesList={categoriesList}
isSubmitting={isSubmitting}
onClose={handleClose}
/>
)}
</Formik>
</DialogContent>
<ItemCategoryForm />
</ItemCategoryProvider>
);
}
export default compose(
withDialogActions,
withItemCategoryDetail(),
withItemCategories(({ categoriesList }) => ({
categoriesList,
})),
withAccounts(({ accountsList }) => ({
accountsList,
})),
withItemCategoriesActions,
withAccountsActions,
)(ItemCategoryFormDialogContent);

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { Classes, FormGroup, InputGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { ErrorMessage, FastField } from 'formik';
import { useAutofocus } from 'hooks';
import { FieldRequiredHint } from 'components';
import { inputIntent } from 'utils';
/**
* Item category form fields.
*/
export default function ItemCategoryFormFields() {
const categoryNameFieldRef = useAutofocus();
return (
<div className={Classes.DIALOG_BODY}>
{/* ----------- Category name ----------- */}
<FastField name={'name'}>
{({ field, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'category_name'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--category-name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="name" />}
inline={true}
>
<InputGroup
medium={true}
inputRef={(ref) => (categoryNameFieldRef.current = ref)}
{...field}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Description ----------- */}
<FastField name={'description'}>
{({ field, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="description" />}
inline={true}
>
<TextArea growVertically={true} large={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Classes, Button, Intent } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { useFormikContext } from 'formik';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { useItemCategoryContext } from './ItemCategoryProvider';
import { compose } from 'utils';
/**
* Item category form footer.
*/
function ItemCategoryFormFooter({
// #withDialogActions
closeDialog,
}) {
// Item category context.
const { isNewMode, dialogName } = useItemCategoryContext();
// Formik context.
const { isSubmitting } = useFormikContext();
// Handle close button click.
const handleCloseBtnClick = () => {
closeDialog(dialogName);
};
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleCloseBtnClick}>
<T id={'close'} />
</Button>
<Button intent={Intent.PRIMARY} type="submit" disabled={isSubmitting}>
{isNewMode ? <T id={'submit'} /> : <T id={'edit'} />}
</Button>
</div>
</div>
);
}
export default compose(withDialogActions)(ItemCategoryFormFooter);

View File

@@ -0,0 +1,57 @@
import React, { createContext } from 'react';
import { DialogContent } from 'components';
import {
useItemCategory,
useEditItemCategory,
useCreateItemCategory,
} from 'hooks/query';
const ItemCategoryContext = createContext();
/**
* Accounts chart data provider.
*/
function ItemCategoryProvider({ itemCategoryId, dialogName, ...props }) {
const { data: itemCategory, isFetching: isItemCategoryLoading } = useItemCategory(
itemCategoryId,
{
enabled: !!itemCategoryId,
},
);
// Create and edit item category mutations.
const { mutateAsync: createItemCategoryMutate } = useCreateItemCategory();
const { mutateAsync: editItemCategoryMutate } = useEditItemCategory();
// Detarmines whether the new mode form.
const isNewMode = !itemCategoryId;
const isEditMode = !isNewMode;
// Provider state.
const provider = {
itemCategoryId,
dialogName,
itemCategory,
isItemCategoryLoading,
createItemCategoryMutate,
editItemCategoryMutate,
isNewMode,
isEditMode
};
return (
<DialogContent
isLoading={isItemCategoryLoading}
name={'item-category-form'}
>
<ItemCategoryContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useItemCategoryContext = () =>
React.useContext(ItemCategoryContext);
export { ItemCategoryProvider, useItemCategoryContext };

View File

@@ -1,5 +1,5 @@
import React, { lazy } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { FormattedMessage as T } from 'react-intl';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';

View File

@@ -7,9 +7,6 @@ const Schema = Yup.object().shape({
.required()
.max(DATATYPES_LENGTH.STRING)
.label(formatMessage({ id: 'category_name_' })),
cost_account_id: Yup.number().nullable(),
sell_account_id: Yup.number().nullable(),
inventory_account_id: Yup.number().nullable(),
description: Yup.string().trim().max(DATATYPES_LENGTH.TEXT).nullable(),
});