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,