diff --git a/src/globalStyles/colors.js b/src/globalStyles/colors.js index 4536e2652..5e0493554 100644 --- a/src/globalStyles/colors.js +++ b/src/globalStyles/colors.js @@ -18,3 +18,4 @@ export const SUSSOL_ORANGE = '#e95c30'; export const TRANSPARENT = 'rgba(0, 0, 0, 0)'; export const WARM_GREY = '#9b9b9b'; export const WARMER_GREY = '#a8aaac'; +export const WHITE = '#FFFFFF'; diff --git a/src/pages/CustomerInvoicePage.js b/src/pages/CustomerInvoicePage.js index ffe3ea5a9..4fb26702c 100644 --- a/src/pages/CustomerInvoicePage.js +++ b/src/pages/CustomerInvoicePage.js @@ -76,17 +76,13 @@ export const CustomerInvoicePage = ({ transaction, runWithLoadingIndicator, rout const onAddMasterList = () => runWithLoadingIndicator(() => dispatch(PageActions.addMasterListItems('Transaction'))); - const renderPageInfo = useCallback( - () => ( - - ), - [comment, theirRef, isFinalised] - ); + const pageInfoColumns = useCallback(getPageInfoColumns(pageObject, dispatch, PageActions), [ + comment, + theirRef, + isFinalised, + ]); - const getAction = (colKey, propName) => { + const getAction = useCallback((colKey, propName) => { switch (colKey) { case 'totalQuantity': return PageActions.editTotalQuantity; @@ -96,7 +92,7 @@ export const CustomerInvoicePage = ({ transaction, runWithLoadingIndicator, rout default: return null; } - }; + }, []); const getModalOnSelect = () => { switch (modalKey) { @@ -114,6 +110,7 @@ export const CustomerInvoicePage = ({ transaction, runWithLoadingIndicator, rout const renderRow = useCallback( listItem => { const { item, index } = listItem; + const rowKey = keyExtractor(item); return ( - {renderPageInfo()} + diff --git a/src/pages/CustomerInvoicesPage.js b/src/pages/CustomerInvoicesPage.js index 1d2ab3d93..e89bc9e85 100644 --- a/src/pages/CustomerInvoicesPage.js +++ b/src/pages/CustomerInvoicesPage.js @@ -73,7 +73,7 @@ export const CustomerInvoicesPage = ({ [showFinalised] ); - const getAction = (colKey, propName) => { + const getAction = useCallback((colKey, propName) => { switch (colKey) { case 'remove': if (propName === 'onCheckAction') return PageActions.selectRow; @@ -81,7 +81,7 @@ export const CustomerInvoicesPage = ({ default: return null; } - }; + }, []); const getModalOnSelect = () => { switch (modalKey) { diff --git a/src/pages/CustomerRequisitionPage.js b/src/pages/CustomerRequisitionPage.js index 0b9f27019..ec565c3b8 100644 --- a/src/pages/CustomerRequisitionPage.js +++ b/src/pages/CustomerRequisitionPage.js @@ -74,24 +74,19 @@ export const CustomerRequisitionPage = ({ requisition, runWithLoadingIndicator, const onSetSuppliedToSuggested = () => runWithLoadingIndicator(() => dispatch(PageActions.setSuppliedToSuggested())); - const renderPageInfo = useCallback( - () => ( - - ), - [comment, isFinalised] - ); + const pageInfoColumns = useCallback(getPageInfoColumns(pageObject, dispatch, PageActions), [ + comment, + isFinalised, + ]); - const getAction = colKey => { + const getAction = useCallback(colKey => { switch (colKey) { case 'suppliedQuantity': return PageActions.editSuppliedQuantity; default: return null; } - }; + }, []); const getModalOnSelect = () => { switch (modalKey) { @@ -145,7 +140,7 @@ export const CustomerRequisitionPage = ({ requisition, runWithLoadingIndicator, - {renderPageInfo()} + diff --git a/src/pages/StocktakeEditPage.js b/src/pages/StocktakeEditPage.js index f0504dc46..8fc883122 100644 --- a/src/pages/StocktakeEditPage.js +++ b/src/pages/StocktakeEditPage.js @@ -94,17 +94,12 @@ export const StocktakeEditPage = ({ const onManageStocktake = () => reduxDispatch(gotoStocktakeManagePage({ stocktake, stocktakeName: stocktake.name })); - const renderPageInfo = useCallback( - () => ( - - ), - [comment, isFinalised] - ); + const pageInfoColumns = useCallback(getPageInfoColumns(pageObject, dispatch, PageActions), [ + comment, + isFinalised, + ]); - const getAction = (colKey, propName) => { + const getAction = useCallback((colKey, propName) => { switch (colKey) { case 'countedTotalQuantity': return PageActions.editCountedQuantity; @@ -118,7 +113,7 @@ export const StocktakeEditPage = ({ default: return null; } - }; + }, []); const getModalOnSelect = () => { switch (modalKey) { @@ -178,7 +173,7 @@ export const StocktakeEditPage = ({ - {renderPageInfo()} + diff --git a/src/pages/StocktakeManagePage.js b/src/pages/StocktakeManagePage.js index 38e2c9971..b85318d17 100644 --- a/src/pages/StocktakeManagePage.js +++ b/src/pages/StocktakeManagePage.js @@ -50,7 +50,7 @@ export const StocktakeManagePage = ({ if (stocktake) dispatch(PageActions.selectItems(stocktake.itemsInStocktake)); }, []); - const getAction = (colKey, propName) => { + const getAction = useCallback((colKey, propName) => { switch (colKey) { case 'selected': if (propName === 'onCheckAction') return PageActions.selectRow; @@ -58,7 +58,7 @@ export const StocktakeManagePage = ({ default: return null; } - }; + }, []); const onFilterData = value => dispatch(PageActions.filterData(value)); const onNameChange = value => dispatch(PageActions.editName(value)); diff --git a/src/pages/StocktakesPage.js b/src/pages/StocktakesPage.js index 48d1e3bdb..9d8ae01ce 100644 --- a/src/pages/StocktakesPage.js +++ b/src/pages/StocktakesPage.js @@ -63,7 +63,7 @@ export const StocktakesPage = ({ routeName, currentUser, dispatch: reduxDispatch return reduxDispatch(gotoStocktakeManagePage({ stocktakeName: '' })); }; - const getAction = (colKey, propName) => { + const getAction = useCallback((colKey, propName) => { switch (colKey) { case 'remove': if (propName === 'onCheckAction') return PageActions.selectRow; @@ -71,7 +71,7 @@ export const StocktakesPage = ({ routeName, currentUser, dispatch: reduxDispatch default: return null; } - }; + }, []); const getModalOnSelect = () => { switch (modalKey) { diff --git a/src/pages/SupplierInvoicePage.js b/src/pages/SupplierInvoicePage.js index 6b6f822ee..76ae77155 100644 --- a/src/pages/SupplierInvoicePage.js +++ b/src/pages/SupplierInvoicePage.js @@ -55,17 +55,13 @@ export const SupplierInvoicePage = ({ routeName, transaction }) => { const onConfirmDelete = () => dispatch(PageActions.deleteTransactionBatches()); const onCloseModal = () => dispatch(PageActions.closeModal()); - const renderPageInfo = useCallback( - () => ( - - ), - [comment, theirRef, isFinalised] - ); + const pageInfoColumns = useCallback(getPageInfoColumns(pageObject, dispatch, PageActions), [ + comment, + theirRef, + isFinalised, + ]); - const getAction = (columnKey, propName) => { + const getAction = useCallback((columnKey, propName) => { switch (columnKey) { case 'totalQuantity': return PageActions.editTotalQuantity; @@ -77,7 +73,7 @@ export const SupplierInvoicePage = ({ routeName, transaction }) => { default: return null; } - }; + }, []); const getModalOnSelect = () => { switch (modalKey) { @@ -134,7 +130,7 @@ export const SupplierInvoicePage = ({ routeName, transaction }) => { - {renderPageInfo()} + diff --git a/src/pages/SupplierInvoicesPage.js b/src/pages/SupplierInvoicesPage.js index bbb40c36c..587bf3bac 100644 --- a/src/pages/SupplierInvoicesPage.js +++ b/src/pages/SupplierInvoicesPage.js @@ -66,7 +66,7 @@ export const SupplierInvoicesPage = ({ onCloseModal(); }; - const getAction = (colKey, propName) => { + const getAction = useCallback((colKey, propName) => { switch (colKey) { case 'remove': if (propName === 'onCheckAction') return PageActions.selectRow; @@ -74,7 +74,7 @@ export const SupplierInvoicesPage = ({ default: return null; } - }; + }, []); const getModalOnSelect = () => { switch (modalKey) { diff --git a/src/pages/SupplierRequisitionPage.js b/src/pages/SupplierRequisitionPage.js index 120c9a696..fa9255277 100644 --- a/src/pages/SupplierRequisitionPage.js +++ b/src/pages/SupplierRequisitionPage.js @@ -88,17 +88,13 @@ export const SupplierRequisitionPage = ({ requisition, runWithLoadingIndicator, const onAddFromMasterList = () => runWithLoadingIndicator(() => dispatch(PageActions.addMasterListItems('Requisition'))); - const renderPageInfo = useCallback( - () => ( - - ), - [comment, theirRef, isFinalised] - ); + const pageInfoColumns = useCallback(getPageInfoColumns(pageObject, dispatch, PageActions), [ + comment, + theirRef, + isFinalised, + ]); - const getAction = (colKey, propName) => { + const getAction = useCallback((colKey, propName) => { switch (colKey) { case 'requiredQuantity': return PageActions.editRequisitionItemRequiredQuantity; @@ -108,7 +104,7 @@ export const SupplierRequisitionPage = ({ requisition, runWithLoadingIndicator, default: return null; } - }; + }, []); const getModalOnSelect = () => { switch (modalKey) { @@ -259,7 +255,7 @@ export const SupplierRequisitionPage = ({ requisition, runWithLoadingIndicator, - {renderPageInfo()} + diff --git a/src/pages/SupplierRequisitionsPage.js b/src/pages/SupplierRequisitionsPage.js index d6797b9a1..a53654337 100644 --- a/src/pages/SupplierRequisitionsPage.js +++ b/src/pages/SupplierRequisitionsPage.js @@ -92,7 +92,7 @@ export const SupplierRequisitionsPage = ({ reduxDispatch(createSupplierRequisition({ ...requisitionParameters, currentUser })); }; - const getAction = (colKey, propName) => { + const getAction = useCallback((colKey, propName) => { switch (colKey) { case 'remove': if (propName === 'onCheckAction') return PageActions.selectRow; @@ -100,7 +100,7 @@ export const SupplierRequisitionsPage = ({ default: return null; } - }; + }, []); const getModalOnSelect = () => { switch (modalKey) { diff --git a/src/widgets/DataTable/CheckableCell.js b/src/widgets/DataTable/CheckableCell.js index 9a4b5cbae..d923459ce 100644 --- a/src/widgets/DataTable/CheckableCell.js +++ b/src/widgets/DataTable/CheckableCell.js @@ -48,12 +48,9 @@ const CheckableCell = React.memo( const onPressAction = isChecked ? onUncheckAction : onCheckAction; - const renderCheck = () => { - if (isDisabled) { - return isChecked ? DisabledCheckedComponent : DisabledUncheckedComponent; - } - return isChecked ? CheckedComponent : UncheckedComponent; - }; + const renderCheck = isChecked + ? (isDisabled && DisabledCheckedComponent) || CheckedComponent + : (isDisabled && DisabledUncheckedComponent) || UncheckedComponent; return ( renderRow(rowItem, focusNextCell, getCellRef, adjustToTop), + [renderRow] + ); + return ( {renderHeader && renderHeader()} @@ -105,7 +110,7 @@ const DataTable = React.memo(({ renderRow, renderHeader, style, data, columns, . data={data} keyboardShouldPersistTaps="always" style={style} - renderItem={rowItem => renderRow(rowItem, focusNextCell, getCellRef, adjustToTop)} + renderItem={renderItem} {...otherProps} /> diff --git a/src/widgets/PageInfo.js b/src/widgets/PageInfo.js index 40b590c56..7c46f4693 100644 --- a/src/widgets/PageInfo.js +++ b/src/widgets/PageInfo.js @@ -111,7 +111,7 @@ const renderInfoComponent = (isEditingDisabled, columnIndex, color, rowData, row * col1: row1 col2: row1 * col1: row2 col2: row2 */ -export const PageInfo = props => { +const PageInfoComponent = props => { const { columns, isEditingDisabled, titleColor, infoColor } = props; return ( @@ -148,16 +148,18 @@ export const PageInfo = props => { ); }; +export const PageInfo = React.memo(PageInfoComponent); + export default PageInfo; -PageInfo.propTypes = { +PageInfoComponent.propTypes = { columns: PropTypes.array.isRequired, isEditingDisabled: PropTypes.bool, titleColor: PropTypes.string, infoColor: PropTypes.string, }; -PageInfo.defaultProps = { +PageInfoComponent.defaultProps = { isEditingDisabled: false, infoColor: SUSSOL_ORANGE, titleColor: DARK_GREY, diff --git a/src/widgets/SearchBar.js b/src/widgets/SearchBar.js index c6dd718ec..4afba4905 100644 --- a/src/widgets/SearchBar.js +++ b/src/widgets/SearchBar.js @@ -18,7 +18,10 @@ import { debounce } from '../utilities/index'; * with a magnifying glass icon and clear button. * * Debounces input by the user, such that the onChangeText callback - * is only invoked once every `debounceTimeout` + * is only invoked once every `debounceTimeout`. + * + * NOTE: This component is exported MEMOIZED. See below for propsAreEqual + * implementation. * * @param {String} color Color of the entire component (monochrome only). * @param {String} onChangeText Callback for changing text (debounced). @@ -30,7 +33,7 @@ import { debounce } from '../utilities/index'; * @param {String} debounceTimeout Time in milliseconds to debounce the onChangeText callback. * @param {Func} onFocusOrBlur Callback for onBlur and onFocus events. */ -export const SearchBar = ({ +export const SearchBarComponent = ({ color, onChangeText, value, @@ -87,13 +90,20 @@ export const SearchBar = ({ /> {!!textValue && ( onChangeTextCallback('')}> - + )} ); }; +/** + * Only re-render this component when the value prop changes. + */ +const propsAreEqual = ({ value: prevValue }, { value: nextValue }) => prevValue === nextValue; + +export const SearchBar = React.memo(SearchBarComponent, propsAreEqual); + const defaultStyles = StyleSheet.create({ container: { borderBottomWidth: 1, @@ -109,7 +119,7 @@ const defaultStyles = StyleSheet.create({ }, }); -SearchBar.defaultProps = { +SearchBarComponent.defaultProps = { debounceTimeout: 250, textInputStyle: defaultStyles.textInput, viewStyle: defaultStyles.container, @@ -120,7 +130,7 @@ SearchBar.defaultProps = { onFocusOrBlur: null, }; -SearchBar.propTypes = { +SearchBarComponent.propTypes = { color: PropTypes.string, debounceTimeout: PropTypes.number, onChangeText: PropTypes.func.isRequired, diff --git a/src/widgets/icons.js b/src/widgets/icons.js index dc1f5b580..fd86e131e 100644 --- a/src/widgets/icons.js +++ b/src/widgets/icons.js @@ -10,37 +10,41 @@ import IonIcon from 'react-native-vector-icons/Ionicons'; import FAIcon from 'react-native-vector-icons/FontAwesome'; import EvilIcon from 'react-native-vector-icons/EvilIcons'; import EntypoIcon from 'react-native-vector-icons/Entypo'; +import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons'; -import { SUSSOL_ORANGE, dataTableColors } from '../globalStyles'; +import { SUSSOL_ORANGE, FINALISED_RED, dataTableColors } from '../globalStyles'; export const SortAscIcon = ; export const SortNeutralIcon = ; export const SortDescIcon = ; -export const CheckedComponent = ( + +export const CheckedComponent = () => ( ); -export const UncheckedComponent = ( + +export const UncheckedComponent = () => ( ); -export const DisabledCheckedComponent = ( + +export const DisabledCheckedComponent = () => ( ); -export const DisabledUncheckedComponent = ( - -); -export const MagnifyingGlass = ({ size, color }) => ( +export const DisabledUncheckedComponent = () => ; + +export const OpenModal = () => ; + +export const MagnifyingGlass = React.memo(({ size, color }) => ( -); +)); + MagnifyingGlass.propTypes = { size: PropTypes.number, color: PropTypes.string }; MagnifyingGlass.defaultProps = { size: 40, color: SUSSOL_ORANGE }; -export const Cancel = ({ color, size }) => ; -Cancel.propTypes = { size: PropTypes.number, color: PropTypes.string }; -Cancel.defaultProps = { size: 40, color: SUSSOL_ORANGE }; -const closeIconStyle = { color: 'white' }; -export const CloseIcon = () => ; +export const Cancel = React.memo(() => ); -export const OpenModal = () => ; +export const CloseIcon = React.memo(() => ); -export const Expand = () => ; +export const Expand = React.memo(() => ( + +)); diff --git a/src/widgets/modals/BottomConfirmModal.js b/src/widgets/modals/BottomConfirmModal.js index 6f1009eb2..7b3ebee9e 100644 --- a/src/widgets/modals/BottomConfirmModal.js +++ b/src/widgets/modals/BottomConfirmModal.js @@ -14,7 +14,7 @@ import { modalStrings } from '../../localization'; import globalStyles, { SUSSOL_ORANGE } from '../../globalStyles'; -export const BottomConfirmModal = props => { +export const BottomConfirmModalComponent = props => { const { onCancel, onConfirm, @@ -44,9 +44,16 @@ export const BottomConfirmModal = props => { ); }; +/** + * Only re-render this component when isOpen prop changes. + */ +const propsAreEqual = ({ isOpen: prevIsOpen }, { isOpen: nextIsOpen }) => prevIsOpen === nextIsOpen; + +export const BottomConfirmModal = React.memo(BottomConfirmModalComponent, propsAreEqual); + export default BottomConfirmModal; -BottomConfirmModal.propTypes = { +BottomConfirmModalComponent.propTypes = { style: ViewPropTypes.style, isOpen: PropTypes.bool.isRequired, questionText: PropTypes.string.isRequired, @@ -55,7 +62,7 @@ BottomConfirmModal.propTypes = { cancelText: PropTypes.string, confirmText: PropTypes.string, }; -BottomConfirmModal.defaultProps = { +BottomConfirmModalComponent.defaultProps = { style: {}, cancelText: modalStrings.cancel, confirmText: modalStrings.confirm, diff --git a/src/widgets/modals/DataTablePageModal.js b/src/widgets/modals/DataTablePageModal.js index f8fc9a22a..5540e4191 100644 --- a/src/widgets/modals/DataTablePageModal.js +++ b/src/widgets/modals/DataTablePageModal.js @@ -1,3 +1,4 @@ +/* eslint-disable import/prefer-default-export */ /* eslint-disable react/forbid-prop-types */ /** * mSupply Mobile @@ -26,6 +27,9 @@ import { modalStrings } from '../../localization'; /** * Wrapper around ModalContainer, containing common modals used in various * DataTable pages. + * + * NOTE: Exported component is MEMOIZED - see below for propsAreEqual implementation. + * * @prop {Bool} fullScreen Force the modal to cover the entire screen. * @prop {Bool} isOpen Whether the modal is open * @prop {Func} onClose A function to call if the close x is pressed @@ -39,7 +43,7 @@ const ADDITIONAL_MODAL_PROPS = { [MODAL_KEYS.ENFORCE_STOCKTAKE_REASON]: { noCancel: true, fullScreen: true }, }; -export const DataTablePageModal = ({ +const DataTablePageModalComponent = ({ fullScreen, isOpen, onClose, @@ -157,7 +161,14 @@ export const DataTablePageModal = ({ ); }; -DataTablePageModal.defaultProps = { +/** + * Only re-render this component when the isOpen prop changes. + */ +const propsAreEqual = ({ isOpen: prevIsOpen }, { isOpen: nextIsOpen }) => prevIsOpen === nextIsOpen; + +export const DataTablePageModal = React.memo(DataTablePageModalComponent, propsAreEqual); + +DataTablePageModalComponent.defaultProps = { fullScreen: false, modalKey: '', onSelect: null, @@ -165,7 +176,7 @@ DataTablePageModal.defaultProps = { modalObject: null, }; -DataTablePageModal.propTypes = { +DataTablePageModalComponent.propTypes = { modalObject: PropTypes.object, fullScreen: PropTypes.bool, isOpen: PropTypes.bool.isRequired, @@ -174,5 +185,3 @@ DataTablePageModal.propTypes = { onSelect: PropTypes.func, currentValue: PropTypes.any, }; - -export default DataTablePageModal; diff --git a/src/widgets/modals/ItemDetails.js b/src/widgets/modals/ItemDetails.js index 88cbebb3d..bb392ee9e 100644 --- a/src/widgets/modals/ItemDetails.js +++ b/src/widgets/modals/ItemDetails.js @@ -28,7 +28,7 @@ import { DARKER_GREY, SUSSOL_ORANGE } from '../../globalStyles'; * @param {Func} onClose Callback for closing the modal. * @param {Any} modalProps Any additional props for the modal component. */ -export const ItemDetails = ({ isOpen, item, onClose, ...modalProps }) => { +export const ItemDetailsComponent = ({ isOpen, item, onClose, ...modalProps }) => { const headers = { batch: 'Batch', expiryDate: 'Expiry', @@ -104,6 +104,21 @@ export const ItemDetails = ({ isOpen, item, onClose, ...modalProps }) => { ); }; +/** + * This component re-renders only when isOpen changes, or the underlying + * item object changes. + */ +const propsAreEqual = ( + { item: prevItem, isOpen: prevIsOpen }, + { item: nextItem, isOpen: nextIsOpen } +) => { + const itemsEqual = prevItem === nextItem; + const isOpenEqual = prevIsOpen === nextIsOpen; + return itemsEqual && isOpenEqual; +}; + +export const ItemDetails = React.memo(ItemDetailsComponent, propsAreEqual); + const localStyles = { scrollView: { height: 170, @@ -116,7 +131,7 @@ const localStyles = { headerRow: { flexDirection: 'row', justifyContent: 'flex-end', marginRight: 10 }, }; -ItemDetails.defaultProps = { +ItemDetailsComponent.defaultProps = { item: null, swipeToClose: false, backdropPressToClose: false, @@ -124,7 +139,7 @@ ItemDetails.defaultProps = { backdrop: false, }; -ItemDetails.propTypes = { +ItemDetailsComponent.propTypes = { item: PropTypes.object, onClose: PropTypes.func.isRequired, isOpen: PropTypes.bool.isRequired,