From 4cd4ff3530f2c4f558c46aa9b641b5031045cdd8 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 11 Nov 2020 15:21:07 +0200 Subject: [PATCH] fix: optimize item form performance. --- .../Accounting/MakeJournalEntriesTable.js | 4 +- client/src/containers/Items/ItemForm.js | 141 ++++----- client/src/containers/Items/ItemFormBody.js | 272 +++++++++--------- .../Items/ItemFormFloatingActions.js | 47 ++- .../Items/ItemFormInventorySection.js | 172 ++++++----- .../Items/ItemFormPrimarySection.js | 210 +++++++------- client/src/utils.js | 7 + 7 files changed, 419 insertions(+), 434 deletions(-) diff --git a/client/src/containers/Accounting/MakeJournalEntriesTable.js b/client/src/containers/Accounting/MakeJournalEntriesTable.js index 516698080..14b6dcae1 100644 --- a/client/src/containers/Accounting/MakeJournalEntriesTable.js +++ b/client/src/containers/Accounting/MakeJournalEntriesTable.js @@ -292,7 +292,7 @@ export default compose( withAccounts(({ accountsList }) => ({ accountsList, })), - withCustomers(({ customersItems }) => ({ - customers: customersItems, + withCustomers(({ customers }) => ({ + customers, })), )(MakeJournalEntriesTable); diff --git a/client/src/containers/Items/ItemForm.js b/client/src/containers/Items/ItemForm.js index 1576aee72..09341f0cc 100644 --- a/client/src/containers/Items/ItemForm.js +++ b/client/src/containers/Items/ItemForm.js @@ -1,9 +1,7 @@ import React, { useMemo, useCallback, useEffect } from 'react'; import * as Yup from 'yup'; -import { useFormik } from 'formik'; -import { - Intent -} from '@blueprintjs/core'; +import { useFormik, Formik, Form } from 'formik'; +import { Intent } from '@blueprintjs/core'; import { queryCache } from 'react-query'; import { useHistory } from 'react-router-dom'; import { pick, pickBy } from 'lodash'; @@ -41,7 +39,6 @@ const defaultInitialValues = { purchasable: true, }; - /** * Item form. */ @@ -88,18 +85,16 @@ function ItemForm({ .required() .label(formatMessage({ id: 'item_type_' })), sku: Yup.string().trim(), - cost_price: Yup.number() - .when(['purchasable'], { - is: true, - then: Yup.number().required(), - otherwise: Yup.number().nullable(true), - }), - sell_price: Yup.number() - .when(['sellable'], { - is: true, - then: Yup.number().required(), - otherwise: Yup.number().nullable(true), - }), + cost_price: Yup.number().when(['purchasable'], { + is: true, + then: Yup.number().required(), + otherwise: Yup.number().nullable(true), + }), + sell_price: Yup.number().when(['sellable'], { + is: true, + then: Yup.number().required(), + otherwise: Yup.number().nullable(true), + }), cost_account_id: Yup.number() .when(['purchasable'], { is: true, @@ -145,21 +140,24 @@ function ItemForm({ ); useEffect(() => { - (!isNewMode) + !isNewMode ? changePageTitle(formatMessage({ id: 'edit_item_details' })) : changePageTitle(formatMessage({ id: 'new_item' })); }, [changePageTitle, isNewMode, formatMessage]); const transformApiErrors = (errors) => { const fields = {}; - if (errors.find(e => e.type === 'ITEM.NAME.ALREADY.EXISTS')) { - fields.name = formatMessage({ id: 'the_name_used_before' }) + if (errors.find((e) => e.type === 'ITEM.NAME.ALREADY.EXISTS')) { + fields.name = formatMessage({ id: 'the_name_used_before' }); } return fields; - } + }; // Handles the form submit. - const handleFormSubmit = (values, { setSubmitting, resetForm, setErrors }) => { + const handleFormSubmit = ( + values, + { setSubmitting, resetForm, setErrors }, + ) => { setSubmitting(true); const form = { ...values }; @@ -167,9 +165,9 @@ function ItemForm({ AppToaster.show({ message: formatMessage( { - id: (isNewMode) ? - 'service_has_been_successful_created' : - 'the_item_has_been_successfully_edited', + id: isNewMode + ? 'service_has_been_successful_created' + : 'the_item_has_been_successfully_edited', }, { number: itemId, @@ -196,28 +194,13 @@ function ItemForm({ } }; - const { - getFieldProps, - setFieldValue, - values, - touched, - errors, - handleSubmit, - isSubmitting, - } = useFormik({ - enableReinitialize: true, - validationSchema: validationSchema, - initialValues, - onSubmit: handleFormSubmit - }); - - useEffect(() => { - if (values.item_type) { - changePageSubtitle(formatMessage({ id: values.item_type })); - } else { - changePageSubtitle(''); - } - }, [values.item_type, changePageSubtitle, formatMessage]); + // useEffect(() => { + // if (values.item_type) { + // changePageSubtitle(formatMessage({ id: values.item_type })); + // } else { + // changePageSubtitle(''); + // } + // }, [values.item_type, changePageSubtitle, formatMessage]); const initialAttachmentFiles = useMemo(() => { return itemDetail && itemDetail.media @@ -228,10 +211,13 @@ function ItemForm({ })) : []; }, [itemDetail]); - - const handleDropFiles = useCallback((_files) => { - setFiles(_files.filter((file) => file.uploaded === false)); - }, [setFiles]); + + const handleDropFiles = useCallback( + (_files) => { + setFiles(_files.filter((file) => file.uploaded === false)); + }, + [setFiles], + ); const handleDeleteFile = useCallback( (_deletedFiles) => { @@ -250,39 +236,30 @@ function ItemForm({ return (
-
-
- - - -
- - + + {({ isSubmitting }) => ( +
+
+ + + +
+ + + )} +
); -}; +} export default compose( withItemsActions, diff --git a/client/src/containers/Items/ItemFormBody.js b/client/src/containers/Items/ItemFormBody.js index c9b07d671..12b92040c 100644 --- a/client/src/containers/Items/ItemFormBody.js +++ b/client/src/containers/Items/ItemFormBody.js @@ -1,15 +1,14 @@ import React from 'react'; +import { FastField, ErrorMessage } from 'formik'; import { FormGroup, Intent, - InputGroup, Classes, Checkbox, } from '@blueprintjs/core'; import { AccountsSelectList, MoneyInputGroup, - ErrorMessage, Col, Row, Hint, @@ -18,161 +17,158 @@ import { FormattedMessage as T } from 'react-intl'; import classNames from 'classnames'; import withAccounts from 'containers/Accounts/withAccounts'; -import { compose } from 'utils'; +import { compose, inputIntent } from 'utils'; /** * Item form body. */ -function ItemFormBody({ - getFieldProps, - touched, - errors, - values, - setFieldValue, - - accountsList, -}) { +function ItemFormBody({ accountsList }) { return (
- {/*------------- Sellable checkbox ------------- */} - - - - - } - checked={values.sellable} - {...getFieldProps('sellable')} - /> - + {/*------------- Purchasable checbox ------------- */} + + {({ field, field: { value } }) => ( + + + + + } + defaultChecked={value} + {...field} + /> + + )} + {/*------------- Selling price ------------- */} - } - className={'form-group--item-selling-price'} - intent={errors.sell_price && touched.sell_price && Intent.DANGER} - helperText={ - - } - inline={true} - > - { - setFieldValue('sell_price', value); - }} - inputGroupProps={{ - medium: true, - intent: - errors.sell_price && touched.sell_price && Intent.DANGER, - }} - disabled={!values.sellable} - /> - - - {/*------------- Selling account ------------- */} - } - labelInfo={} - inline={true} - intent={ - errors.sell_account_id && touched.sell_account_id && Intent.DANGER - } - helperText={ - - } - className={classNames( - 'form-group--sell-account', - 'form-group--select-list', - Classes.FILL, + + {({ form, field, field: { value }, meta: { error, touched } }) => ( + } + className={'form-group--sell_price'} + intent={inputIntent({ error, touched })} + helperText={} + inline={true} + > + + )} - > - { - setFieldValue('sell_account_id', account.id); - }} - defaultSelectText={} - selectedAccountId={values.sell_account_id} - disabled={!values.sellable} - filterByTypes={['income']} - /> - + + + {/*------------- Selling account ------------- */} + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + inline={true} + intent={inputIntent({ error, touched })} + helperText={} + className={classNames( + 'form-group--sell-account', + 'form-group--select-list', + Classes.FILL, + )} + > + { + form.setFieldValue('sell_account_id', account.id); + }} + defaultSelectText={} + selectedAccountId={value} + disabled={!form.values.sellable} + filterByTypes={['income']} + /> + + )} + - {/*------------- Purchasable checbox ------------- */} - - - - - } - defaultChecked={values.purchasable} - {...getFieldProps('purchasable')} - /> - + {/*------------- Sellable checkbox ------------- */} + + {({ field, field: { value }, meta: { error, touched } }) => ( + + + + + } + checked={value} + {...field} + /> + + )} + {/*------------- Cost price ------------- */} - } - className={'form-group--item-cost-price'} - intent={errors.cost_price && touched.cost_price && Intent.DANGER} - helperText={ - - } - inline={true} - > - { - setFieldValue('cost_price', value); - }} - inputGroupProps={{ - medium: true, - intent: - errors.cost_price && touched.cost_price && Intent.DANGER, - }} - disabled={!values.purchasable} - /> - + + {({ field, form, field: { value }, meta: { error, touched } }) => ( + } + className={'form-group--item-cost-price'} + intent={inputIntent({ error, touched })} + helperText={} + inline={true} + > + + + )} + {/*------------- Cost account ------------- */} - } - labelInfo={} - inline={true} - intent={ - errors.cost_account_id && touched.cost_account_id && Intent.DANGER - } - helperText={ - - } - className={classNames( - 'form-group--cost-account', - 'form-group--select-list', - Classes.FILL, + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + inline={true} + intent={inputIntent({ error, touched })} + helperText={} + className={classNames( + 'form-group--cost-account', + 'form-group--select-list', + Classes.FILL, + )} + > + { + form.setFieldValue('cost_account_id', account.id); + }} + defaultSelectText={} + selectedAccountId={value} + disabled={!form.values.purchasable} + filterByTypes={['cost_of_goods_sold']} + /> + )} - > - { - setFieldValue('cost_account_id', account.id); - }} - defaultSelectText={} - selectedAccountId={values.cost_account_id} - disabled={!values.purchasable} - filterByTypes={['cost_of_goods_sold']} - /> - +
diff --git a/client/src/containers/Items/ItemFormFloatingActions.js b/client/src/containers/Items/ItemFormFloatingActions.js index 82297b60d..7e0a7eff0 100644 --- a/client/src/containers/Items/ItemFormFloatingActions.js +++ b/client/src/containers/Items/ItemFormFloatingActions.js @@ -1,24 +1,15 @@ -import React from 'react'; -import { - Button, - Intent, - FormGroup, - Checkbox -} from '@blueprintjs/core'; +import React, { memo } from 'react'; +import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core'; import { FormattedMessage as T } from 'react-intl'; import { saveInvoke } from 'utils'; import classNames from 'classnames'; - +import { ErrorMessage, FastField } from 'formik'; import { CLASSES } from 'common/classes'; /** * Item form floating actions. */ -export default function ItemFormFloatingActions({ - isSubmitting, - itemId, - onCancelClick -}) { +export default function ItemFormFloatingActions({ isSubmitting, itemId, onCancelClick }) { const handleCancelBtnClick = (event) => { saveInvoke(onCancelClick, event.currentTarget.value); }; @@ -37,18 +28,24 @@ export default function ItemFormFloatingActions({ {/*----------- Active ----------*/} - - } - // defaultChecked={values.active} - // {...getFieldProps('active')} - /> - + + {({ field, field: { value } }) => ( + + } + defaultChecked={value} + {...field} + /> + + )} + ); } + +// function areEqual(prevProps, nextProps) { +// return prevProps.isSubmitting === nextProps.isSubmitting; +// } + +// export default memo(ItemFormFloatingActions, areEqual); diff --git a/client/src/containers/Items/ItemFormInventorySection.js b/client/src/containers/Items/ItemFormInventorySection.js index 6321e7200..af246f10d 100644 --- a/client/src/containers/Items/ItemFormInventorySection.js +++ b/client/src/containers/Items/ItemFormInventorySection.js @@ -1,30 +1,18 @@ import React from 'react'; -import { - FormGroup, - Intent, - InputGroup, - Position, -} from '@blueprintjs/core'; +import { Field, FastField, ErrorMessage } from 'formik'; +import { FormGroup, Intent, InputGroup, Position } from '@blueprintjs/core'; import { DateInput } from '@blueprintjs/datetime'; -import { AccountsSelectList, ErrorMessage, Col, Row, Hint } from 'components'; +import { AccountsSelectList, Col, Row, Hint } from 'components'; import { CLASSES } from 'common/classes'; import { FormattedMessage as T } from 'react-intl'; import classNames from 'classnames'; import withAccounts from 'containers/Accounts/withAccounts'; -import { compose, tansformDateValue, momentFormatter } from 'utils'; +import { compose, tansformDateValue, momentFormatter, inputIntent } from 'utils'; /** * Item form inventory sections. */ -function ItemFormInventorySection({ - errors, - touched, - setFieldValue, - values, - getFieldProps, - - accountsList, -}) { +function ItemFormInventorySection({ accountsList }) { return (

@@ -34,83 +22,91 @@ function ItemFormInventorySection({ {/*------------- Inventory account ------------- */} - } - inline={true} - intent={ - errors.inventory_account_id && - touched.inventory_account_id && - Intent.DANGER - } - helperText={ - - } - className={classNames( - 'form-group--item-inventory_account', - 'form-group--select-list', - CLASSES.FILL, + + {({ form, field: { value }, meta: { touched, error } }) => ( + } + inline={true} + intent={inputIntent({ error, touched })} + helperText={} + className={classNames( + 'form-group--item-inventory_account', + 'form-group--select-list', + CLASSES.FILL, + )} + > + { + form.setFieldValue('inventory_account_id', account.id); + }} + defaultSelectText={} + selectedAccountId={value} + /> + )} - > - { - setFieldValue('inventory_account_id', account.id); - }} - defaultSelectText={} - selectedAccountId={values.inventory_account_id} - /> - + - } - labelInfo={} - className={'form-group--opening_quantity'} - inline={true} - > - - - - } - labelInfo={} - className={classNames( - 'form-group--select-list', - 'form-group--opening_date', - CLASSES.FILL, + + {({ field, field: { value }, meta: { touched, error } }) => ( + } + labelInfo={} + className={'form-group--opening_quantity'} + intent={inputIntent({ error, touched })} + inline={true} + > + + )} - inline={true} - > - { - setFieldValue('opening_date', value); - }} - popoverProps={{ position: Position.BOTTOM, minimal: true }} - /> - + + + + {({ form, field: { value }, meta: { touched, error } }) => ( + } + labelInfo={} + className={classNames( + 'form-group--select-list', + 'form-group--opening_date', + CLASSES.FILL, + )} + intent={inputIntent({ error, touched })} + inline={true} + > + { + form.setFieldValue('opening_date', value); + }} + popoverProps={{ position: Position.BOTTOM, minimal: true }} + /> + + )} + - } - className={'form-group--opening_average_rate'} - inline={true} - > - - + + {({ field, field: { value }, meta: { touched, error } }) => ( + } + className={'form-group--opening_average_rate'} + intent={inputIntent({ error, touched })} + inline={true} + > + + + )} +

diff --git a/client/src/containers/Items/ItemFormPrimarySection.js b/client/src/containers/Items/ItemFormPrimarySection.js index dd90e3c7d..e63ec625d 100644 --- a/client/src/containers/Items/ItemFormPrimarySection.js +++ b/client/src/containers/Items/ItemFormPrimarySection.js @@ -7,12 +7,11 @@ import { Classes, Radio, Position, - Tooltip, } from '@blueprintjs/core'; import { FormattedMessage as T } from 'react-intl'; +import { ErrorMessage, FastField } from 'formik'; import { CategoriesSelectList, - ErrorMessage, Hint, Col, Row, @@ -24,126 +23,139 @@ import { CLASSES } from 'common/classes'; import withItemCategories from 'containers/Items/withItemCategories'; import withAccounts from 'containers/Accounts/withAccounts'; -import { compose, handleStringChange } from 'utils'; +import { compose, handleStringChange, inputIntent } from 'utils'; /** * Item form primary section. */ function ItemFormPrimarySection({ - getFieldProps, - setFieldValue, - errors, - touched, - values, - // #withItemCategories categoriesList, }) { - const itemTypeHintContent = ( <> -
{'Service: '}{'Services that you provide to customers. '}
-
{'Inventory: '}{'Products you buy and/or sell and that you track quantities of.'}
-
{'Non-Inventory: '}{'Products you buy and/or sell but don’t need to (or can’t) track quantities of, for example, nuts and bolts used in an installation.'}
- ); +
+ {'Service: '} + {'Services that you provide to customers. '} +
+
+ {'Inventory: '} + {'Products you buy and/or sell and that you track quantities of.'} +
+
+ {'Non-Inventory: '} + { + 'Products you buy and/or sell but don’t need to (or can’t) track quantities of, for example, nuts and bolts used in an installation.' + } +
+ + ); return (
{/*----------- Item type ----------*/} - } - labelInfo={ - - - - - } - className={'form-group--item-type'} - intent={errors.type && touched.type && Intent.DANGER} - helperText={} - inline={true} - > - { - setFieldValue('type', value); - })} - selectedValue={values.type} - > - } - value="service" - /> - } - value="inventory" - /> - } - value="non-inventory" - /> - - + + {({ form, field: { value }, meta: { touched, error } }) => ( + } + labelInfo={ + + + + + } + className={'form-group--item-type'} + intent={inputIntent({ error, touched })} + helperText={} + inline={true} + > + { + form.setFieldValue('type', _value); + })} + selectedValue={value} + > + } value="service" /> + } value="inventory" /> + } + value="non-inventory" + /> + + + )} + {/*----------- Item name ----------*/} - } - labelInfo={} - className={'form-group--item-name'} - intent={errors.name && touched.name && Intent.DANGER} - helperText={} - inline={true} - > - - + + {({ field, meta: { error, touched } }) => ( + } + labelInfo={} + className={'form-group--item-name'} + intent={inputIntent({ error, touched })} + helperText={} + inline={true} + > + + + )} + {/*----------- SKU ----------*/} - } - labelInfo={} - className={'form-group--item-sku'} - intent={errors.sku && touched.sku && Intent.DANGER} - helperText={} - inline={true} - > - - + + {({ field, meta: { error, touched } }) => ( + } + labelInfo={} + className={' -group--item-sku'} + intent={inputIntent({ error, touched })} + helperText={} + inline={true} + > + + + )} + {/*----------- Item category ----------*/} - } - labelInfo={} - inline={true} - intent={errors.category_id && touched.category_id && Intent.DANGER} - helperText={ - - } - className={classNames( - 'form-group--select-list', - 'form-group--category', - Classes.FILL, + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + inline={true} + intent={inputIntent({ error, touched })} + helperText={} + className={classNames( + 'form-group--select-list', + 'form-group--category', + Classes.FILL, + )} + > + { + form.setFieldValue('category_id', category.id); + }} + popoverProps={{ minimal: true }} + /> + )} - > - { - setFieldValue('category_id', category.id); - }} - popoverProps={{ minimal: true }} - /> - + diff --git a/client/src/utils.js b/client/src/utils.js index 8abe4c145..b888dbc10 100644 --- a/client/src/utils.js +++ b/client/src/utils.js @@ -1,5 +1,8 @@ import moment from 'moment'; import _ from 'lodash'; +import { + Intent, +} from '@blueprintjs/core'; import Currency from 'js-money/lib/currency'; import PProgress from 'p-progress'; import accounting from 'accounting'; @@ -284,4 +287,8 @@ export const transformToForm = (obj, emptyInitialValues) => { obj, (val, key) => val !== null && Object.keys(emptyInitialValues).includes(key), ) +} + +export function inputIntent({ error, touched }){ + return error && touched ? Intent.DANGER : ''; } \ No newline at end of file