From 82e62cb4a7e02b5517c8c4fcdb25b54f027db09e Mon Sep 17 00:00:00 2001 From: Guillermo Espinosa Date: Tue, 23 Jul 2024 18:24:34 -0400 Subject: [PATCH] fix queryParams unconnected --- CHANGELOG.md | 2 +- packages/ui/cypress/e2e/fieldFilter.spec.js | 1 + packages/ui/demo/src/examples/DataTable.js | 3 +- packages/ui/package.json | 2 +- packages/ui/src/DataTable/ColumnFilterMenu.js | 34 +- packages/ui/src/DataTable/Columns.js | 130 +- .../ui/src/DataTable/FilterAndSortMenu.js | 445 +++-- packages/ui/src/DataTable/index.js | 1443 ++++++++++------- packages/ui/src/DataTable/utils/index.js | 4 +- .../ui/src/DataTable/utils/queryParams.js | 16 +- .../src/DataTable/utils/useDeepEqualMemo.js | 10 + .../ui/src/DataTable/utils/withTableParams.js | 51 +- packages/ui/src/FormComponents/Uploader.js | 42 +- packages/ui/src/FormComponents/index.js | 1 + packages/ui/src/MatchHeaders.js | 4 +- packages/ui/src/UploadCsvWizard.js | 432 +++-- packages/ui/src/useDialog.js | 47 +- 17 files changed, 1468 insertions(+), 1199 deletions(-) create mode 100644 packages/ui/src/DataTable/utils/useDeepEqualMemo.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 61549ec2..117e1adf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Added menu item to allow user to export DNA as FASTA when looking at a protein sequence to resolve https://github.com/TeselaGen/tg-oss/issues/61 [`#61`](https://github.com/TeselaGen/tg-oss/issues/61) - Added fix for clipboard commands when there are multiple editors on a page to resolve https://github.com/TeselaGen/tg-oss/issues/24 [`#24`](https://github.com/TeselaGen/tg-oss/issues/24) - closes #35 [`#35`](https://github.com/TeselaGen/tg-oss/issues/35) -- refactor [`ec2050c`](https://github.com/TeselaGen/tg-oss/commit/ec2050cfce60e27cb9fc3ef6242303ded2e7ef5e) +- refactor [`2ebddf6`](https://github.com/TeselaGen/tg-oss/commit/2ebddf65509b90255198fb3c3659143f431c6d3f) - updating yarn lock [`ff41df0`](https://github.com/TeselaGen/tg-oss/commit/ff41df0b49b8051fcba084f7eaa44cf23284926d) - updating deps, moving to lodash-es, moving all packages to type: module [`bc7312c`](https://github.com/TeselaGen/tg-oss/commit/bc7312ccbe27c2d9a11cf2563ba978199428b50a) diff --git a/packages/ui/cypress/e2e/fieldFilter.spec.js b/packages/ui/cypress/e2e/fieldFilter.spec.js index b7d60476..48b40c1c 100644 --- a/packages/ui/cypress/e2e/fieldFilter.spec.js +++ b/packages/ui/cypress/e2e/fieldFilter.spec.js @@ -14,6 +14,7 @@ describe("field filters", () => { cy.get(".bp3-popover input").type(firstAge); cy.get(".bp3-popover").contains("Filter").click(); cy.get(".rt-tr-group.with-row-data").should("have.length.at.least", 1); + cy.get(".rt-tr-group.with-row-data").should("have.length.at.most", 10); }); }); diff --git a/packages/ui/demo/src/examples/DataTable.js b/packages/ui/demo/src/examples/DataTable.js index 045175d1..b3500c8f 100644 --- a/packages/ui/demo/src/examples/DataTable.js +++ b/packages/ui/demo/src/examples/DataTable.js @@ -529,8 +529,7 @@ const DataTableDemo = () => { expandAllByDefault={expandAllByDefault} extraCompact={extraCompact} {...(getRowClassName && { - getRowClassName: rowInfo => { - console.info(`rowInfo:`, rowInfo); + getRowClassName: () => { return { "custom-getRowClassName": true }; diff --git a/packages/ui/package.json b/packages/ui/package.json index d16e36f6..644107cd 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@teselagen/ui", - "version": "0.4.19-beta.7", + "version": "0.4.19-beta.13", "main": "./src/index.js", "type": "module", "exports": { diff --git a/packages/ui/src/DataTable/ColumnFilterMenu.js b/packages/ui/src/DataTable/ColumnFilterMenu.js index 1688fa32..d3d227b3 100644 --- a/packages/ui/src/DataTable/ColumnFilterMenu.js +++ b/packages/ui/src/DataTable/ColumnFilterMenu.js @@ -29,32 +29,32 @@ export const ColumnFilterMenu = ({ hide: { enabled: false }, flip: { enabled: false } }} + content={ + { + setColumnFilterMenuOpen(false); + }} + /> + } > { - setColumnFilterMenuOpen(!columnFilterMenuOpen); - }} + onClick={() => setColumnFilterMenuOpen(prev => !prev)} className={classNames("tg-filter-menu-button", { "tg-active-filter": !!filterActiveForColumn })} /> - { - setColumnFilterMenuOpen(false); - }} - /> ); }; diff --git a/packages/ui/src/DataTable/Columns.js b/packages/ui/src/DataTable/Columns.js index 818ee87a..ba216598 100644 --- a/packages/ui/src/DataTable/Columns.js +++ b/packages/ui/src/DataTable/Columns.js @@ -42,7 +42,7 @@ import { getCCDisplayName } from "./utils/queryParams"; dayjs.extend(localizedFormat); -const renderColumnHeader = ({ +const RenderColumnHeader = ({ addFilters, column, compact, @@ -114,54 +114,8 @@ const renderColumnHeader = ({ const sortDown = ordering && ordering === "asc"; const sortUp = ordering && !sortDown; - const sortComponent = - withSort && !disableSorting ? ( -
- { - setOrder("-" + ccDisplayName, sortUp, e.shiftKey); - }} - /> - { - setOrder(ccDisplayName, sortDown, e.shiftKey); - }} - /> -
- ) : null; const FilterMenu = column.FilterMenu || FilterAndSortMenu; - const filterMenu = - withFilter && !disableFiltering ? ( - - ) : null; let maybeCheckbox; if (isCellEditable && !isNotEditable && type === "boolean") { let isIndeterminate = false; @@ -239,8 +193,50 @@ const renderColumnHeader = ({
- {sortComponent} - {filterMenu} + {withSort && !disableSorting && ( +
+ { + setOrder("-" + ccDisplayName, sortUp, e.shiftKey); + }} + /> + { + setOrder(ccDisplayName, sortDown, e.shiftKey); + }} + /> +
+ )} + {withFilter && !disableFiltering && ( + + )}
); @@ -487,7 +483,7 @@ const RenderCell = ({ ); }; -export const renderColumns = props => { +export const RenderColumns = props => { const { addFilters, cellRenderer, @@ -506,6 +502,7 @@ export const renderColumns = props => { isCellEditable, isEntityDisabled, isLocalCall, + isSimple, isSingleSelect, isSelectionARectangle, noDeselectAll, @@ -538,10 +535,12 @@ export const renderColumns = props => { updateValidation, withCheckboxes, withExpandAndCollapseAllButton, - withFilter = !props.isSimple, + withFilter: _withFilter, withSort = true } = props; + const withFilter = _withFilter === undefined ? !isSimple : _withFilter; + const onDragEnd = cellsToSelect => { const [primaryRowId, primaryCellPath] = primarySelectedCellId.split(":"); const pathToField = getFieldPathToField(schema); @@ -742,6 +741,8 @@ export const renderColumns = props => { // TODOCOPY we need a way to potentially omit certain columns from being added as a \t element (talk to taoh about this) let text = typeof val !== "string" ? row.value : val; + // We should try to take out the props from here, it produces + // unnecessary rerenders const record = row.original; if (column.getClipboardData) { text = column.getClipboardData(row.value, record, row, props); @@ -787,6 +788,7 @@ export const renderColumns = props => { if (!columns.length) { return columns; } + const columnsToRender = []; if (SubComponent) { columnsToRender.push({ @@ -948,7 +950,7 @@ export const renderColumns = props => { columns.forEach(column => { const tableColumn = { ...column, - Header: renderColumnHeader({ + Header: RenderColumnHeader({ column, isLocalCall, filters, @@ -994,17 +996,17 @@ export const renderColumns = props => { return val; }; } else if (column.type === "timestamp") { - tableColumn.Cell = props => { - return props.value ? dayjs(props.value).format("lll") : ""; + tableColumn.Cell = ({ value }) => { + return value ? dayjs(value).format("lll") : ""; }; } else if (column.type === "color") { - tableColumn.Cell = props => { - return props.value ? ( + tableColumn.Cell = ({ value }) => { + return value ? (
{ }; } else if (column.type === "boolean") { if (isCellEditable) { - tableColumn.Cell = props => (props.value ? "True" : "False"); + tableColumn.Cell = ({ value }) => (value ? "True" : "False"); } else { - tableColumn.Cell = props => ( + tableColumn.Cell = ({ value }) => ( ); } } else if (column.type === "markdown") { - tableColumn.Cell = props => ( - {props.value} + tableColumn.Cell = ({ value }) => ( + {value} ); } else { - tableColumn.Cell = props => props.value; + tableColumn.Cell = ({ value }) => value; } const oldFunc = tableColumn.Cell; diff --git a/packages/ui/src/DataTable/FilterAndSortMenu.js b/packages/ui/src/DataTable/FilterAndSortMenu.js index af121cd6..94191e7b 100644 --- a/packages/ui/src/DataTable/FilterAndSortMenu.js +++ b/packages/ui/src/DataTable/FilterAndSortMenu.js @@ -1,7 +1,7 @@ +import React, { useState } from "react"; import { DateInput, DateRangeInput } from "@blueprintjs/datetime"; import { camelCase } from "lodash-es"; import classNames from "classnames"; -import React from "react"; import { Menu, Intent, @@ -54,42 +54,38 @@ const isInvalidFilterValue = value => { return value === "" || value === undefined || value.length === 0; }; -export default class FilterAndSortMenu extends React.Component { - constructor(props) { - super(props); - const selectedFilter = camelCase(getFilterMenuItems(props.dataType)[0]); - this.state = { - selectedFilter, - filterValue: "", - ...this.props.currentFilter - }; - } - handleFilterChange = selectedFilter => { - const { filterValue } = this.state; +const FilterAndSortMenu = ({ + dataType, + togglePopover, + filterOn, + addFilters, + removeSingleFilter, + currentFilter +}) => { + const [selectedFilter, setSelectedFilter] = useState( + camelCase(getFilterMenuItems(dataType)[0]) + ); + const [filterValue, setFilterValue] = useState(""); + + const handleFilterChange = selectedFilter => { if ( filterValue && !Array.isArray(filterValue) && filterTypesDictionary[selectedFilter] === "list" ) { - this.setState({ - filterValue: filterValue?.split(" ") || [] - }); + setFilterValue(filterValue?.split(" ") || []); } else if ( filterTypesDictionary[selectedFilter] === "text" && Array.isArray(filterValue) ) { - this.setState({ - filterValue: filterValue.join(" ") - }); + setFilterValue(filterValue.join(" ")); } - this.setState({ selectedFilter: camelCase(selectedFilter) }); - }; - handleFilterValueChange = filterValue => { - this.setState({ filterValue }); + setSelectedFilter(camelCase(selectedFilter)); }; - handleFilterSubmit = () => { - const { filterValue, selectedFilter } = this.state; - const { togglePopover, dataType } = this.props; + + const handleFilterValueChange = filterValue => setFilterValue(filterValue); + + const handleFilterSubmit = () => { const ccSelectedFilter = camelCase(selectedFilter); let filterValToUse = filterValue; if (ccSelectedFilter === "true" || ccSelectedFilter === "false") { @@ -112,7 +108,6 @@ export default class FilterAndSortMenu extends React.Component { } } - const { filterOn, addFilters, removeSingleFilter } = this.props; if (isInvalidFilterValue(filterValToUse)) { togglePopover(); return removeSingleFilter(filterOn); @@ -126,71 +121,62 @@ export default class FilterAndSortMenu extends React.Component { ]); togglePopover(); }; - // handleSubmit(event) { - // alert('A name was submitted: ' + this.state.value); - // event.preventDefault(); - // } - render() { - const { selectedFilter, filterValue } = this.state; - const { dataType, currentFilter, removeSingleFilter } = this.props; - const { handleFilterChange, handleFilterValueChange, handleFilterSubmit } = - this; + const filterMenuItems = getFilterMenuItems(dataType); + const ccSelectedFilter = camelCase(selectedFilter); + const requiresValue = ccSelectedFilter && ccSelectedFilter !== "none"; - const filterMenuItems = getFilterMenuItems(dataType); - const ccSelectedFilter = camelCase(selectedFilter); - const requiresValue = ccSelectedFilter && ccSelectedFilter !== "none"; - - return ( - -
-
- -
-
-
- + return ( + +
+
+
- - { - handleFilterSubmit(); - }} - intent={Intent.SUCCESS} - text="Filter" - secondaryText="Clear" - secondaryIntent={Intent.DANGER} - secondaryAction={() => { - currentFilter && removeSingleFilter(currentFilter.filterOn); - }} +
+
+ -
- ); - } -} +
+ + { + handleFilterSubmit(); + }} + intent={Intent.SUCCESS} + text="Filter" + secondaryText="Clear" + secondaryIntent={Intent.DANGER} + secondaryAction={() => { + currentFilter && removeSingleFilter(currentFilter.filterOn); + }} + /> +
+ ); +}; + +export default FilterAndSortMenu; const dateMinMaxHelpers = { minDate: dayjs().subtract(25, "years").toDate(), @@ -205,69 +191,27 @@ const renderCreateNewOption = (query, active, handleClick) => ( shouldDismissPopover={false} /> ); -class FilterInput extends React.Component { - render() { - const { - handleFilterValueChange, - handleFilterSubmit, - filterValue, - filterSubType, - filterType - } = this.props; - //Options: Text, Single number (before, after, equals), 2 numbers (range), - //Single Date (before, after, on), 2 dates (range) - let inputGroup =
; - switch (filterType) { - case "text": - inputGroup = - filterSubType === "notEmpty" || filterSubType === "isEmpty" ? ( -
- ) : ( -
- -
- ); - break; - case "list": - inputGroup = ( -
- ({ - label: val, - value: val - }))} - onChange={selectedOptions => { - selectedOptions.some(opt => opt.value === "") - ? handleFilterSubmit() - : handleFilterValueChange( - selectedOptions.map(opt => opt.value) - ); - }} - options={[]} - /> -
- ); - break; - case "number": - inputGroup = ( +const FilterInput = ({ + handleFilterValueChange, + handleFilterSubmit, + filterValue, + filterSubType, + filterType +}) => { + //Options: Text, Single number (before, after, equals), 2 numbers (range), + //Single Date (before, after, on), 2 dates (range) + let inputGroup =
; + switch (filterType) { + case "text": + inputGroup = + filterSubType === "notEmpty" || filterSubType === "isEmpty" ? ( +
+ ) : (
-
); - break; - case "numberRange": - inputGroup = ( -
- - -
- ); - break; - case "date": - inputGroup = ( -
- { + break; + case "list": + inputGroup = ( +
+ ({ + label: val, + value: val + }))} + onChange={selectedOptions => { + selectedOptions.some(opt => opt.value === "") + ? handleFilterSubmit() + : handleFilterValueChange( + selectedOptions.map(opt => opt.value) + ); + }} + options={[]} + /> +
+ ); + break; + case "number": + inputGroup = ( +
+ +
+ ); + break; + case "numberRange": + inputGroup = ( +
+ + +
+ ); + break; + case "date": + inputGroup = ( +
+ { + handleFilterValueChange(selectedDates); + }} + /> +
+ ); + break; + case "dateRange": + // eslint-disable-next-line no-case-declarations + let filterValueToUse; + if (Array.isArray(filterValue)) { + filterValueToUse = filterValue; + } else { + filterValueToUse = + filterValue && filterValue.split && filterValue.split(";"); + } + inputGroup = ( +
+ (date == null ? "" : date.toLocaleDateString())} + parseDate={str => new Date(Date.parse(str))} + placeholder="JS Date" + {...dateMinMaxHelpers} + onChange={selectedDates => { + if (selectedDates[0] && selectedDates[1]) { handleFilterValueChange(selectedDates); - }} - /> -
- ); - break; - case "dateRange": - // eslint-disable-next-line no-case-declarations - let filterValueToUse; - if (Array.isArray(filterValue)) { - filterValueToUse = filterValue; - } else { - filterValueToUse = - filterValue && filterValue.split && filterValue.split(";"); - } - inputGroup = ( -
- - date == null ? "" : date.toLocaleDateString(), - parseDate: str => new Date(Date.parse(str)), - placeholder: "JS Date" - }} - {...dateMinMaxHelpers} - onChange={selectedDates => { - if (selectedDates[0] && selectedDates[1]) { - handleFilterValueChange(selectedDates); - } - }} - /> -
- ); - break; - default: - // to do - } - return inputGroup; + }} + /> +
+ ); + break; + default: + // to do } -} + return inputGroup; +}; function getFilterMenuItems(dataType) { let filterMenuItems = []; diff --git a/packages/ui/src/DataTable/index.js b/packages/ui/src/DataTable/index.js index 9e0d49e0..14b9cc02 100644 --- a/packages/ui/src/DataTable/index.js +++ b/packages/ui/src/DataTable/index.js @@ -44,7 +44,7 @@ import scrollIntoView from "dom-scroll-into-view"; import ReactTable from "@teselagen/react-table"; import immer, { produceWithPatches, enablePatches, applyPatches } from "immer"; import papaparse from "papaparse"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; import { ThComponent } from "./ThComponent"; import { @@ -65,7 +65,8 @@ import { handleCopyRows, handleCopyTable, isEntityClean, - PRIMARY_SELECTED_VAL + PRIMARY_SELECTED_VAL, + useDeepEqualMemo } from "./utils"; import rowClick, { finalizeSelection } from "./utils/rowClick"; import PagingTool from "./PagingTool"; @@ -92,7 +93,7 @@ import { makeDataTableHandlers, setCurrentParamsOnUrl } from "./utils/queryParams"; -import { renderColumns } from "./Columns"; +import { RenderColumns } from "./Columns"; import { formValueSelector } from "redux-form"; enablePatches(); @@ -115,16 +116,16 @@ const DataTable = ({ orderByFirstColumn, schema: __schema, showEmptyColumnsByDefault, - tableParams, + tableParams: _tableParams, + anyTouched, + blur, ...ownProps }) => { - if (isTableParamsConnected && tableParams && !tableParams.entities) { + if (isTableParamsConnected && _tableParams && !_tableParams.entities) { throw new Error( `No entities array detected in tableParams object (). You need to call withQuery() after withTableParams() like: compose(withTableParams(), withQuery(something)).` ); } - - const dispatch = useDispatch(); const tableRef = useRef(); const alreadySelected = useRef(false); const [editableCellValue, setEditableCellValue] = useState(""); @@ -154,11 +155,11 @@ const DataTable = ({ }); const { - reduxFormCellValidation, - reduxFormEntities, - reduxFormQueryParams = {}, - reduxFormSearchInput = "", - reduxFormSelectedEntityIdMap = {} + reduxFormCellValidation: _reduxFormCellValidation, + reduxFormEntities: _reduxFormEntities, + reduxFormQueryParams: _reduxFormQueryParams = {}, + reduxFormSearchInput: _reduxFormSearchInput = "", + reduxFormSelectedEntityIdMap: _reduxFormSelectedEntityIdMap = {} } = useSelector(state => formValueSelector(formName)( state, @@ -170,12 +171,22 @@ const DataTable = ({ ) ); + // We want to make sure we don't rerender everything unnecessary + // with redux-forms we tend to do unnecessary renders + const reduxFormCellValidation = useDeepEqualMemo(_reduxFormCellValidation); + const reduxFormEntities = useDeepEqualMemo(_reduxFormEntities); + const reduxFormQueryParams = useDeepEqualMemo(_reduxFormQueryParams); + const reduxFormSearchInput = useDeepEqualMemo(_reduxFormSearchInput); + const reduxFormSelectedEntityIdMap = useDeepEqualMemo( + _reduxFormSelectedEntityIdMap + ); + let props = ownProps; if (!isTableParamsConnected) { //this is the case where we're hooking up to withTableParams locally, so we need to take the tableParams off the props props = { ...ownProps, - ...tableParams + ..._tableParams }; } @@ -247,28 +258,30 @@ const DataTable = ({ }) }; - const currentParams = urlConnected - ? getCurrentParamsFromUrl(history.location) //important to use history location and not ownProps.location because for some reason the location path lags one render behind!! - : reduxFormQueryParams; + const currentParams = useMemo(() => { + const tmp = + (urlConnected + ? getCurrentParamsFromUrl(history.location) //important to use history location and not ownProps.location because for some reason the location path lags one render behind!! + : reduxFormQueryParams) || {}; + + tmp.searchTerm = reduxFormSearchInput; + return tmp; + }, [ + history.location, + reduxFormQueryParams, + reduxFormSearchInput, + urlConnected + ]); if (!isTableParamsConnected) { const updateSearch = val => { - setTimeout(() => { - dispatch(change(formName, "reduxFormSearchInput", val || "")); - }); + change("reduxFormSearchInput", val || ""); }; - let setNewParams; - if (urlConnected) { - setNewParams = newParams => { - setCurrentParamsOnUrl(newParams, history.replace); - dispatch(change(formName, "reduxFormQueryParams", newParams)); //we always will update the redux params as a workaround for withRouter not always working if inside a redux-connected container https://github.com/ReactTraining/react-router/issues/5037 - }; - } else { - setNewParams = function (newParams) { - dispatch(change(formName, "reduxFormQueryParams", newParams)); - }; - } + const setNewParams = newParams => { + urlConnected && setCurrentParamsOnUrl(newParams, history.replace); + change("reduxFormQueryParams", newParams); //we always will update the redux params as a workaround for withRouter not always working if inside a redux-connected container https://github.com/ReactTraining/react-router/issues/5037 + }; const bindThese = makeDataTableHandlers({ setNewParams, @@ -286,12 +299,12 @@ const DataTable = ({ }; }); - const changeFormValue = (...args) => dispatch(change(formName, ...args)); + const changeFormValue = (...args) => change(...args); props.tableParams = { changeFormValue, selectedEntities, - ...tableParams, + ..._tableParams, ...props, ...boundDispatchProps, isTableParamsConnected: true //let the table know not to do local sorting/filtering etc. @@ -303,19 +316,17 @@ const DataTable = ({ ...props.tableParams }; - const additionalFilterToUse = - typeof props.additionalFilter === "function" - ? props.additionalFilter.bind(this, ownProps) - : () => props.additionalFilter; - - const additionalOrFilterToUse = - typeof props.additionalOrFilter === "function" - ? props.additionalOrFilter.bind(this, ownProps) - : () => props.additionalOrFilter; + if (!isTableParamsConnected) { + const additionalFilterToUse = + typeof props.additionalFilter === "function" + ? props.additionalFilter.bind(this, ownProps) + : () => props.additionalFilter; - currentParams.searchTerm = reduxFormSearchInput; + const additionalOrFilterToUse = + typeof props.additionalOrFilter === "function" + ? props.additionalOrFilter.bind(this, ownProps) + : () => props.additionalOrFilter; - if (!isTableParamsConnected) { props = { ...props, ...getQueryParams({ @@ -360,6 +371,7 @@ const DataTable = ({ disableSetPageSize, doNotShowEmptyRows, doNotValidateUntouchedRows, + editingCellSelectAll, entities: _origEntities = [], entitiesAcrossPages, entityCount, @@ -369,6 +381,7 @@ const DataTable = ({ extraCompact: _extraCompact, filters = [], fragment, + getCellHoverText, getRowClassName, hideColumnHeader, hideDisplayOptionsIcon, @@ -396,6 +409,7 @@ const DataTable = ({ noPadding = isSimple, noRowsFoundMessage, noSelect = false, + noUserSelect, onDeselect = noop, onDoubleClick, onMultiRowSelect = noop, @@ -403,14 +417,18 @@ const DataTable = ({ onRowClick = noop, onRowSelect = noop, onSingleRowSelect = noop, + order, page = 1, pageSize: _pageSize = 10, pagingDisabled, + removeSingleFilter, ReactTableProps = {}, safeQuery, searchMenuButton, searchTerm, selectAllByDefault, + setNewParams, + setOrder, setPage = noop, setPageSize = noop, setSearchTerm = noop, @@ -427,16 +445,19 @@ const DataTable = ({ variables, withCheckboxes = false, withDisplayOptions, + withExpandAndCollapseAllButton, + withFilter, withPaging = !isSimple, withSearch = !isSimple, withSelectAll, + withSort, withTitle = !isSimple } = props; // We need to memoize the entities so that we don't rerender the table - const entities = reduxFormEntities?.length - ? reduxFormEntities - : _origEntities; + const entities = useDeepEqualMemo( + (reduxFormEntities?.length ? reduxFormEntities : _origEntities) || [] + ); const [tableConfig, setTableConfig] = useState({ fieldOptions: [] }); @@ -462,9 +483,7 @@ const DataTable = ({ withDisplayOptions ]); - // This shouldn't depend on the entities, we should look into a - // way to separate this into smaller chunks. - const { schema } = useMemo(() => { + const schema = useMemo(() => { const schema = convertSchema(_schema); if (isViewable) { schema.fields = [viewColumn, ...schema.fields]; @@ -502,7 +521,7 @@ const DataTable = ({ const val = get(e, field.path); return field.render ? !field.render(val, e) - : cellRenderer[field.path] + : cellRenderer?.[field.path] ? !cellRenderer[field.path](val, e) : !val; }); @@ -559,18 +578,18 @@ const DataTable = ({ } } } - return { schema }; - // eslint-disable-next-line react-hooks/exhaustive-deps + return schema; }, [ _schema, cellRenderer, - // entities + entities, isInfinite, isOpenable, isSimple, isViewable, showForcedHiddenColumns, - tableConfig, + tableConfig.columnOrderings, + tableConfig.fieldOptions, withDisplayOptions ]); @@ -693,7 +712,10 @@ const DataTable = ({ extraCompact = tableConfig.density === "extraCompact"; } - const resized = tableConfig.resized || []; + const resized = useMemo( + () => tableConfig.resized || [], + [tableConfig?.resized] + ); const pageSize = controlled_pageSize || _pageSize; @@ -716,7 +738,8 @@ const DataTable = ({ }); change("reduxFormCellValidation", tableWideErr); }, - [change, schema] + // eslint-disable-next-line react-hooks/exhaustive-deps + [schema] ); const updateEntitiesHelper = useCallback( @@ -737,7 +760,8 @@ const DataTable = ({ } })); }, - [change] + // eslint-disable-next-line react-hooks/exhaustive-deps + [] ); const formatAndValidateEntities = useCallback( @@ -1297,8 +1321,14 @@ const DataTable = ({ }); }; isCellEditable && formatAndValidateTableInitial(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isCellEditable]); + }, [ + change, + entities, + formatAndValidateEntities, + isCellEditable, + reduxFormCellValidation, + updateValidation + ]); const handlePaste = useCallback( e => { @@ -1582,36 +1612,40 @@ const DataTable = ({ } }, [initialSelectedIds, setSelectedIds]); - const moveColumn = ({ oldIndex, newIndex }) => { - let oldStateColumnIndex, newStateColumnIndex; - columns.forEach((column, i) => { - if (oldIndex === column.columnIndex) oldStateColumnIndex = i; - if (newIndex === column.columnIndex) newStateColumnIndex = i; - }); - // because it is all handled in state we need - // to perform the move and update the columnIndices - // because they are used for the sortable columns - const newColumns = arrayMove( - columns, - oldStateColumnIndex, - newStateColumnIndex - ).map((column, i) => { - return { - ...column, - columnIndex: i + const TheadComponent = useCallback( + ({ className, style, children }) => { + const moveColumn = ({ oldIndex, newIndex }) => { + let oldStateColumnIndex, newStateColumnIndex; + columns.forEach((column, i) => { + if (oldIndex === column.columnIndex) oldStateColumnIndex = i; + if (newIndex === column.columnIndex) newStateColumnIndex = i; + }); + // because it is all handled in state we need + // to perform the move and update the columnIndices + // because they are used for the sortable columns + const newColumns = arrayMove( + columns, + oldStateColumnIndex, + newStateColumnIndex + ).map((column, i) => { + return { + ...column, + columnIndex: i + }; + }); + setColumns(newColumns); }; - }); - setColumns(newColumns); - }; - - const TheadComponent = ({ className, style, children }) => ( - - {children} - + return ( + + {children} + + ); + }, + [columns, moveColumnPersist] ); const addEntitiesToSelection = entities => { @@ -1644,7 +1678,7 @@ const DataTable = ({ table?.focus(); }; - const isSelectionARectangle = () => { + const isSelectionARectangle = useCallback(() => { if (selectedCells && Object.keys(selectedCells).length > 1) { const pathToIndex = getFieldPathToIndex(schema); const entityMap = getEntityIdToEntity(entities); @@ -1712,480 +1746,561 @@ const DataTable = ({ } } return {}; - }; + }, [entities, schema, selectedCells]); - const handleCellClick = ({ event, cellId }) => { - if (!cellId) return; - const [rowId, cellPath] = cellId.split(":"); - const entityMap = getEntityIdToEntity(entities); - const { e: entity, i: rowIndex } = entityMap[rowId]; - const pathToIndex = getFieldPathToIndex(schema); - const columnIndex = pathToIndex[cellPath]; - const rowDisabled = isEntityDisabled(entity); - - if (rowDisabled) return; - let newSelectedCells = { - ...selectedCells - }; - if (newSelectedCells[cellId] && !event.shiftKey) { - // don't deselect if editing - if (editingCell === cellId) return; - if (event.metaKey) { - delete newSelectedCells[cellId]; - } else { - newSelectedCells = {}; - newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - } - } else { - if (event.metaKey) { - if (isEmpty(newSelectedCells)) { - newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; + const handleCellClick = useCallback( + ({ event, cellId }) => { + if (!cellId) return; + const [rowId, cellPath] = cellId.split(":"); + const entityMap = getEntityIdToEntity(entities); + const { e: entity, i: rowIndex } = entityMap[rowId]; + const pathToIndex = getFieldPathToIndex(schema); + const columnIndex = pathToIndex[cellPath]; + const rowDisabled = isEntityDisabled(entity); + + if (rowDisabled) return; + let newSelectedCells = { + ...selectedCells + }; + if (newSelectedCells[cellId] && !event.shiftKey) { + // don't deselect if editing + if (editingCell === cellId) return; + if (event.metaKey) { + delete newSelectedCells[cellId]; } else { + newSelectedCells = {}; newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - if (primarySelectedCellId) - newSelectedCells[primarySelectedCellId] = true; } - } else if (event.shiftKey) { - if (primarySelectedCellId) { - const [rowId, colPath] = primarySelectedCellId.split(":"); - const primaryRowIndex = entities.findIndex((e, i) => { - return getIdOrCodeOrIndex(e, i) === rowId; - }); - const fieldToIndex = getFieldPathToIndex(schema); - const primaryColIndex = fieldToIndex[colPath]; - - if (primaryRowIndex === -1 || primaryColIndex === -1) { - newSelectedCells = {}; + } else { + if (event.metaKey) { + if (isEmpty(newSelectedCells)) { newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; } else { - const minRowIndex = min([primaryRowIndex, rowIndex]); - const minColIndex = min([primaryColIndex, columnIndex]); - const maxRowIndex = max([primaryRowIndex, rowIndex]); - const maxColIndex = max([primaryColIndex, columnIndex]); - const entitiesBetweenRows = entities.slice( - minRowIndex, - maxRowIndex + 1 - ); - const fieldsBetweenCols = schema.fields.slice( - minColIndex, - maxColIndex + 1 - ); - newSelectedCells = { - [primarySelectedCellId]: PRIMARY_SELECTED_VAL - }; - entitiesBetweenRows.forEach(e => { - const rowId = getIdOrCodeOrIndex(e, entities.indexOf(e)); - fieldsBetweenCols.forEach(f => { - const cellId = `${rowId}:${f.path}`; - if (!newSelectedCells[cellId]) newSelectedCells[cellId] = true; - }); + newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; + if (primarySelectedCellId) + newSelectedCells[primarySelectedCellId] = true; + } + } else if (event.shiftKey) { + if (primarySelectedCellId) { + const [rowId, colPath] = primarySelectedCellId.split(":"); + const primaryRowIndex = entities.findIndex((e, i) => { + return getIdOrCodeOrIndex(e, i) === rowId; }); - // newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - // newSelectedCells[primarySelectedCellId] = true; + const fieldToIndex = getFieldPathToIndex(schema); + const primaryColIndex = fieldToIndex[colPath]; + + if (primaryRowIndex === -1 || primaryColIndex === -1) { + newSelectedCells = {}; + newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; + } else { + const minRowIndex = min([primaryRowIndex, rowIndex]); + const minColIndex = min([primaryColIndex, columnIndex]); + const maxRowIndex = max([primaryRowIndex, rowIndex]); + const maxColIndex = max([primaryColIndex, columnIndex]); + const entitiesBetweenRows = entities.slice( + minRowIndex, + maxRowIndex + 1 + ); + const fieldsBetweenCols = schema.fields.slice( + minColIndex, + maxColIndex + 1 + ); + newSelectedCells = { + [primarySelectedCellId]: PRIMARY_SELECTED_VAL + }; + entitiesBetweenRows.forEach(e => { + const rowId = getIdOrCodeOrIndex(e, entities.indexOf(e)); + fieldsBetweenCols.forEach(f => { + const cellId = `${rowId}:${f.path}`; + if (!newSelectedCells[cellId]) + newSelectedCells[cellId] = true; + }); + }); + // newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; + // newSelectedCells[primarySelectedCellId] = true; + } + } else { + newSelectedCells = {}; + newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; } } else { newSelectedCells = {}; newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; } - } else { - newSelectedCells = {}; - newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; } - } - setSelectedCells(newSelectedCells); - }; - - const insertRows = ({ above, numRows = 1, appendToBottom } = {}) => { - const [rowId] = primarySelectedCellId?.split(":") || []; - updateEntitiesHelper(entities, entities => { - const newEntities = times(numRows).map(() => ({ id: nanoid() })); + setSelectedCells(newSelectedCells); + }, + [ + editingCell, + entities, + isEntityDisabled, + primarySelectedCellId, + schema, + selectedCells + ] + ); - const indexToInsert = entities.findIndex((e, i) => { - return getIdOrCodeOrIndex(e, i) === rowId; - }); - const insertIndex = above ? indexToInsert : indexToInsert + 1; - const insertIndexToUse = appendToBottom ? entities.length : insertIndex; - let { newEnts, validationErrors } = formatAndValidateEntities( - newEntities, - { - useDefaultValues: true, - indexToStartAt: insertIndexToUse - } - ); + const insertRows = useCallback( + ({ above, numRows = 1, appendToBottom } = {}) => { + const [rowId] = primarySelectedCellId?.split(":") || []; + updateEntitiesHelper(entities, entities => { + const newEntities = times(numRows).map(() => ({ id: nanoid() })); - newEnts = newEnts.map(e => ({ - ...e, - _isClean: true - })); - updateValidation(entities, { - ...reduxFormCellValidation, - ...validationErrors - }); + const indexToInsert = entities.findIndex((e, i) => { + return getIdOrCodeOrIndex(e, i) === rowId; + }); + const insertIndex = above ? indexToInsert : indexToInsert + 1; + const insertIndexToUse = appendToBottom ? entities.length : insertIndex; + let { newEnts, validationErrors } = formatAndValidateEntities( + newEntities, + { + useDefaultValues: true, + indexToStartAt: insertIndexToUse + } + ); - entities.splice(insertIndexToUse, 0, ...newEnts); - }); - refocusTable(); - }; + newEnts = newEnts.map(e => ({ + ...e, + _isClean: true + })); + updateValidation(entities, { + ...reduxFormCellValidation, + ...validationErrors + }); - const showContextMenu = (e, { idMap, selectedCells } = {}) => { - let selectedRecords; - if (isCellEditable) { - const rowIds = {}; - Object.keys(selectedCells).forEach(cellKey => { - const [rowId] = cellKey.split(":"); - rowIds[rowId] = true; + entities.splice(insertIndexToUse, 0, ...newEnts); }); - selectedRecords = entities.filter(ent => rowIds[getIdOrCodeOrIndex(ent)]); - } else { - selectedRecords = getRecordsFromIdMap(idMap); - } + refocusTable(); + }, + [ + entities, + formatAndValidateEntities, + primarySelectedCellId, + reduxFormCellValidation, + updateEntitiesHelper, + updateValidation + ] + ); - const itemsToRender = contextMenu({ - selectedRecords, - history - }); - if (!itemsToRender && !isCopyable) return null; - const copyMenuItems = []; - - e.persist(); - if (isCopyable) { - //compute the cellWrapper here so we don't lose access to it - const cellWrapper = - e.target.querySelector(".tg-cell-wrapper") || - e.target.closest(".tg-cell-wrapper"); - if (cellWrapper) { - copyMenuItems.push( - { - //TODOCOPY: we need to make sure that the cell copy is being used by the row copy.. right now we have 2 different things going on - //do we need to be able to copy hidden cells? It seems like it should just copy what's on the page..? - const specificColumn = cellWrapper.getAttribute("data-test"); - handleCopyRows([cellWrapper.closest(".rt-tr")], { - specificColumn, - onFinishMsg: "Cell copied" - }); - const [text, jsonText] = getCellCopyText(cellWrapper); - handleCopyHelper(text, jsonText); - }} - text="Cell" - /> + const showContextMenu = useCallback( + (e, { idMap, selectedCells } = {}) => { + let selectedRecords; + if (isCellEditable) { + const rowIds = {}; + Object.keys(selectedCells).forEach(cellKey => { + const [rowId] = cellKey.split(":"); + rowIds[rowId] = true; + }); + selectedRecords = entities.filter( + ent => rowIds[getIdOrCodeOrIndex(ent)] ); + } else { + selectedRecords = getRecordsFromIdMap(idMap); + } - copyMenuItems.push( - { - handleCopyColumn(e, cellWrapper); - }} - text="Column" - /> - ); - if (selectedRecords.length > 1) { + const itemsToRender = contextMenu({ + selectedRecords, + history + }); + if (!itemsToRender && !isCopyable) return null; + const copyMenuItems = []; + + e.persist(); + if (isCopyable) { + //compute the cellWrapper here so we don't lose access to it + const cellWrapper = + e.target.querySelector(".tg-cell-wrapper") || + e.target.closest(".tg-cell-wrapper"); + if (cellWrapper) { copyMenuItems.push( { - handleCopyColumn(e, cellWrapper, selectedRecords); + //TODOCOPY: we need to make sure that the cell copy is being used by the row copy.. right now we have 2 different things going on + //do we need to be able to copy hidden cells? It seems like it should just copy what's on the page..? + const specificColumn = cellWrapper.getAttribute("data-test"); + handleCopyRows([cellWrapper.closest(".rt-tr")], { + specificColumn, + onFinishMsg: "Cell copied" + }); + const [text, jsonText] = getCellCopyText(cellWrapper); + handleCopyHelper(text, jsonText); }} - text="Column (Selected)" + text="Cell" /> ); - } - } - if (selectedRecords.length === 0 || selectedRecords.length === 1) { - //compute the row here so we don't lose access to it - const cell = - e.target.querySelector(".tg-cell-wrapper") || - e.target.closest(".tg-cell-wrapper") || - e.target.closest(".rt-td"); - const row = cell.closest(".rt-tr"); - copyMenuItems.push( - { - handleCopyRows([row]); - // loop through each cell in the row - }} - text="Row" - /> - ); - } else if (selectedRecords.length > 1) { - copyMenuItems.push( - { - handleCopySelectedRows(selectedRecords, e); - // loop through each cell in the row - }} - text="Rows" - /> - ); - } - copyMenuItems.push( - { - handleCopyTable(e); - // loop through each cell in the row - }} - text="Table" - /> - ); - } - const selectedRowIds = Object.keys(selectedCells).map(cellId => { - const [rowId] = cellId.split(":"); - return rowId; - }); - const menu = ( - - {itemsToRender} - {copyMenuItems.length && ( - - {copyMenuItems} - - )} - {isCellEditable && ( - <> + copyMenuItems.push( { - insertRows({ above: true }); + handleCopyColumn(e, cellWrapper); }} + text="Column" /> + ); + if (selectedRecords.length > 1) { + copyMenuItems.push( + { + handleCopyColumn(e, cellWrapper, selectedRecords); + }} + text="Column (Selected)" + /> + ); + } + } + if (selectedRecords.length === 0 || selectedRecords.length === 1) { + //compute the row here so we don't lose access to it + const cell = + e.target.querySelector(".tg-cell-wrapper") || + e.target.closest(".tg-cell-wrapper") || + e.target.closest(".rt-td"); + const row = cell.closest(".rt-tr"); + copyMenuItems.push( { - insertRows({}); + handleCopyRows([row]); + // loop through each cell in the row }} + text="Row" /> + ); + } else if (selectedRecords.length > 1) { + copyMenuItems.push( 1 ? "s" : ""}`} - key="removeRow" + key="copySelectedRows" onClick={() => { - const selectedRowIds = Object.keys(selectedCells).map( - cellId => { - const [rowId] = cellId.split(":"); - return rowId; - } - ); - updateEntitiesHelper(entities, entities => { - const ents = entities.filter( - (e, i) => !selectedRowIds.includes(getIdOrCodeOrIndex(e, i)) - ); - updateValidation( - ents, - omitBy(reduxFormCellValidation, (v, cellId) => - selectedRowIds.includes(cellId.split(":")[0]) - ) - ); - return ents; - }); - refocusTable(); + handleCopySelectedRows(selectedRecords, e); + // loop through each cell in the row }} + text="Rows" /> - - )} - - ); - ContextMenu.show(menu, { left: e.clientX, top: e.clientY }); - }; + ); + } + copyMenuItems.push( + { + handleCopyTable(e); + // loop through each cell in the row + }} + text="Table" + /> + ); + } + const selectedRowIds = Object.keys(selectedCells).map(cellId => { + const [rowId] = cellId.split(":"); + return rowId; + }); + + const menu = ( + + {itemsToRender} + {copyMenuItems.length && ( + + {copyMenuItems} + + )} + {isCellEditable && ( + <> + { + insertRows({ above: true }); + }} + /> + { + insertRows({}); + }} + /> + 1 ? "s" : ""}`} + key="removeRow" + onClick={() => { + const selectedRowIds = Object.keys(selectedCells).map( + cellId => { + const [rowId] = cellId.split(":"); + return rowId; + } + ); + updateEntitiesHelper(entities, entities => { + const ents = entities.filter( + (e, i) => + !selectedRowIds.includes(getIdOrCodeOrIndex(e, i)) + ); + updateValidation( + ents, + omitBy(reduxFormCellValidation, (v, cellId) => + selectedRowIds.includes(cellId.split(":")[0]) + ) + ); + return ents; + }); + refocusTable(); + }} + /> + + )} + + ); + ContextMenu.show(menu, { left: e.clientX, top: e.clientY }); + }, + [ + contextMenu, + entities, + handleCopySelectedRows, + history, + insertRows, + isCellEditable, + isCopyable, + reduxFormCellValidation, + updateEntitiesHelper, + updateValidation + ] + ); - const getTableRowProps = (state, rowInfo) => { - if (!rowInfo) { + const getTableRowProps = useCallback( + (state, rowInfo) => { + if (!rowInfo) { + return { + className: "no-row-data" + }; + } + const entity = rowInfo.original; + const rowId = getIdOrCodeOrIndex(entity, rowInfo.index); + const rowSelected = reduxFormSelectedEntityIdMap[rowId]; + const isExpanded = expandedEntityIdMap[rowId]; + const rowDisabled = isEntityDisabled(entity); + const dataId = entity.id || entity.code; return { - className: "no-row-data" - }; - } - const entity = rowInfo.original; - const rowId = getIdOrCodeOrIndex(entity, rowInfo.index); - const rowSelected = reduxFormSelectedEntityIdMap[rowId]; - const isExpanded = expandedEntityIdMap[rowId]; - const rowDisabled = isEntityDisabled(entity); - const dataId = entity.id || entity.code; - return { - onClick: e => { - if (isCellEditable) return; - // if checkboxes are activated or row expander is clicked don't select row - if (e.target.matches(".tg-expander, .tg-expander *")) { - setExpandedEntityIdMap(prev => ({ ...prev, [rowId]: !isExpanded })); - return; - } else if ( - e.target.closest(".tg-react-table-checkbox-cell-container") - ) { - return; - } else if (mustClickCheckboxToSelect) { - return; - } - if (e.detail > 1) { - return; //cancel multiple quick clicks - } - rowClick(e, rowInfo, entities, { - reduxFormSelectedEntityIdMap, - isSingleSelect, - noSelect, - onRowClick, - isEntityDisabled, - withCheckboxes, - onDeselect, - onSingleRowSelect, - onMultiRowSelect, - noDeselectAll, - onRowSelect, - change - }); - }, - //row right click - onContextMenu: e => { - e.preventDefault(); - if (rowId === undefined || rowDisabled || isCellEditable) return; - const oldIdMap = cloneDeep(reduxFormSelectedEntityIdMap) || {}; - let newIdMap; - if (withCheckboxes) { - newIdMap = oldIdMap; - } else { - // if we are not using checkboxes we need to make sure - // that the id of the record gets added to the id map - newIdMap = oldIdMap[rowId] ? oldIdMap : { [rowId]: { entity } }; - - // tgreen: this will refresh the selection with fresh data. The entities in redux might not be up to date - const keyedEntities = keyBy(entities, getIdOrCodeOrIndex); - forEach(newIdMap, (val, key) => { - const freshEntity = keyedEntities[key]; - if (freshEntity) { - newIdMap[key] = { ...newIdMap[key], entity: freshEntity }; - } - }); - finalizeSelection({ - idMap: newIdMap, - entities, - props: { - onDeselect, - onSingleRowSelect, - onMultiRowSelect, - noDeselectAll, - onRowSelect, - noSelect, - change - } + onClick: e => { + if (isCellEditable) return; + // if checkboxes are activated or row expander is clicked don't select row + if (e.target.matches(".tg-expander, .tg-expander *")) { + setExpandedEntityIdMap(prev => ({ ...prev, [rowId]: !isExpanded })); + return; + } else if ( + e.target.closest(".tg-react-table-checkbox-cell-container") + ) { + return; + } else if (mustClickCheckboxToSelect) { + return; + } + if (e.detail > 1) { + return; //cancel multiple quick clicks + } + rowClick(e, rowInfo, entities, { + reduxFormSelectedEntityIdMap, + isSingleSelect, + noSelect, + onRowClick, + isEntityDisabled, + withCheckboxes, + onDeselect, + onSingleRowSelect, + onMultiRowSelect, + noDeselectAll, + onRowSelect, + change }); + }, + //row right click + onContextMenu: e => { + e.preventDefault(); + if (rowId === undefined || rowDisabled || isCellEditable) return; + const oldIdMap = cloneDeep(reduxFormSelectedEntityIdMap) || {}; + let newIdMap; + if (withCheckboxes) { + newIdMap = oldIdMap; + } else { + // if we are not using checkboxes we need to make sure + // that the id of the record gets added to the id map + newIdMap = oldIdMap[rowId] ? oldIdMap : { [rowId]: { entity } }; + + // tgreen: this will refresh the selection with fresh data. The entities in redux might not be up to date + const keyedEntities = keyBy(entities, getIdOrCodeOrIndex); + forEach(newIdMap, (val, key) => { + const freshEntity = keyedEntities[key]; + if (freshEntity) { + newIdMap[key] = { ...newIdMap[key], entity: freshEntity }; + } + }); + finalizeSelection({ + idMap: newIdMap, + entities, + props: { + onDeselect, + onSingleRowSelect, + onMultiRowSelect, + noDeselectAll, + onRowSelect, + noSelect, + change + } + }); + } + showContextMenu(e, { idMap: newIdMap, selectedCells }); + }, + className: classNames( + "with-row-data", + getRowClassName && getRowClassName(rowInfo, state, props), + { + disabled: rowDisabled, + selected: rowSelected && !withCheckboxes, + "rt-tr-last-row": rowInfo.index === entities.length - 1 + } + ), + "data-test-id": dataId === undefined ? rowInfo.index : dataId, + "data-index": rowInfo.index, + onDoubleClick: e => { + if (rowDisabled) return; + onDoubleClick && + onDoubleClick(rowInfo.original, rowInfo.index, history, e); } - showContextMenu(e, { idMap: newIdMap, selectedCells }); - }, - className: classNames( - "with-row-data", - getRowClassName && getRowClassName(rowInfo, state, props), - { - disabled: rowDisabled, - selected: rowSelected && !withCheckboxes, - "rt-tr-last-row": rowInfo.index === entities.length - 1 - } - ), - "data-test-id": dataId === undefined ? rowInfo.index : dataId, - "data-index": rowInfo.index, - onDoubleClick: e => { - if (rowDisabled) return; - onDoubleClick && - onDoubleClick(rowInfo.original, rowInfo.index, history, e); - } - }; - }; - - const getTableCellProps = (state, rowInfo, column) => { - if (!isCellEditable) return {}; //only allow cell selection to do stuff here - if (!rowInfo) return {}; - if (!reduxFormCellValidation) return {}; - const entity = rowInfo.original; - const rowIndex = rowInfo.index; - const rowId = getIdOrCodeOrIndex(entity, rowIndex); - const { - cellId, - cellIdAbove, - cellIdToRight, - cellIdBelow, - cellIdToLeft, - rowDisabled, - columnIndex - } = getCellInfo({ - columnIndex: column.index, - columnPath: column.path, - rowId, - schema, + }; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ entities, - rowIndex, + expandedEntityIdMap, + getRowClassName, + history, + isCellEditable, isEntityDisabled, - entity - }); + isSingleSelect, + mustClickCheckboxToSelect, + noDeselectAll, + noSelect, + onDeselect, + onDoubleClick, + onMultiRowSelect, + onRowClick, + onRowSelect, + onSingleRowSelect, + props, + reduxFormSelectedEntityIdMap, + selectedCells, + showContextMenu, + withCheckboxes + ] + ); - const _isClean = - (entity._isClean && doNotValidateUntouchedRows) || isEntityClean(entity); - - const err = !_isClean && reduxFormCellValidation[cellId]; - let selectedTopBorder, - selectedRightBorder, - selectedBottomBorder, - selectedLeftBorder; - if (selectedCells[cellId]) { - selectedTopBorder = !selectedCells[cellIdAbove]; - selectedRightBorder = !selectedCells[cellIdToRight]; - selectedBottomBorder = !selectedCells[cellIdBelow]; - selectedLeftBorder = !selectedCells[cellIdToLeft]; - } - const isPrimarySelected = selectedCells[cellId] === PRIMARY_SELECTED_VAL; - const className = classNames({ - isSelectedCell: selectedCells[cellId], - isPrimarySelected, - isSecondarySelected: selectedCells[cellId] === true, - noSelectedTopBorder: !selectedTopBorder, - isCleanRow: _isClean, - noSelectedRightBorder: !selectedRightBorder, - noSelectedBottomBorder: !selectedBottomBorder, - noSelectedLeftBorder: !selectedLeftBorder, - isDropdownCell: column.type === "dropdown", - isEditingCell: editingCell === cellId, - hasCellError: !!err, - "no-data-tip": selectedCells[cellId] - }); - return { - onDoubleClick: () => { - // cell double click - if (rowDisabled) return; - startCellEdit(cellId); - }, - ...(err && { - "data-tip": err?.message || err, - "data-no-child-data-tip": true - }), - onContextMenu: e => { - const newSelectedCells = { ...selectedCells }; - if (!isPrimarySelected) { - if (primarySelectedCellId) { - newSelectedCells[primarySelectedCellId] = true; + const getTableCellProps = useCallback( + (state, rowInfo, column) => { + if (!isCellEditable) return {}; //only allow cell selection to do stuff here + if (!rowInfo) return {}; + if (!reduxFormCellValidation) return {}; + const entity = rowInfo.original; + const rowIndex = rowInfo.index; + const rowId = getIdOrCodeOrIndex(entity, rowIndex); + const { + cellId, + cellIdAbove, + cellIdToRight, + cellIdBelow, + cellIdToLeft, + rowDisabled, + columnIndex + } = getCellInfo({ + columnIndex: column.index, + columnPath: column.path, + rowId, + schema, + entities, + rowIndex, + isEntityDisabled, + entity + }); + + const _isClean = + (entity._isClean && doNotValidateUntouchedRows) || + isEntityClean(entity); + + const err = !_isClean && reduxFormCellValidation[cellId]; + let selectedTopBorder, + selectedRightBorder, + selectedBottomBorder, + selectedLeftBorder; + if (selectedCells[cellId]) { + selectedTopBorder = !selectedCells[cellIdAbove]; + selectedRightBorder = !selectedCells[cellIdToRight]; + selectedBottomBorder = !selectedCells[cellIdBelow]; + selectedLeftBorder = !selectedCells[cellIdToLeft]; + } + const isPrimarySelected = selectedCells[cellId] === PRIMARY_SELECTED_VAL; + const className = classNames({ + isSelectedCell: selectedCells[cellId], + isPrimarySelected, + isSecondarySelected: selectedCells[cellId] === true, + noSelectedTopBorder: !selectedTopBorder, + isCleanRow: _isClean, + noSelectedRightBorder: !selectedRightBorder, + noSelectedBottomBorder: !selectedBottomBorder, + noSelectedLeftBorder: !selectedLeftBorder, + isDropdownCell: column.type === "dropdown", + isEditingCell: editingCell === cellId, + hasCellError: !!err, + "no-data-tip": selectedCells[cellId] + }); + return { + onDoubleClick: () => { + // cell double click + if (rowDisabled) return; + startCellEdit(cellId); + }, + ...(err && { + "data-tip": err?.message || err, + "data-no-child-data-tip": true + }), + onContextMenu: e => { + const newSelectedCells = { ...selectedCells }; + if (!isPrimarySelected) { + if (primarySelectedCellId) { + newSelectedCells[primarySelectedCellId] = true; + } + newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; + setSelectedCells(newSelectedCells); } - newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - setSelectedCells(newSelectedCells); - } - showContextMenu(e, { selectedCells: newSelectedCells }); - }, - onClick: event => { - handleCellClick({ - event, - cellId, - rowDisabled, - rowIndex, - columnIndex - }); - }, - className - }; - }; + showContextMenu(e, { selectedCells: newSelectedCells }); + }, + onClick: event => { + handleCellClick({ + event, + cellId, + rowDisabled, + rowIndex, + columnIndex + }); + }, + className + }; + }, + [ + doNotValidateUntouchedRows, + editingCell, + entities, + handleCellClick, + isCellEditable, + isEntityDisabled, + primarySelectedCellId, + reduxFormCellValidation, + schema, + selectedCells, + showContextMenu, + startCellEdit + ] + ); if (withSelectAll && !safeQuery) { throw new Error("safeQuery is needed for selecting all table records"); @@ -2382,6 +2497,201 @@ const DataTable = ({ }); } + // We are nnot rerendering when props and change are changed, + // we need to figure out how to manage them correctly + const renderColumns = useMemo( + () => + RenderColumns({ + ...props, + addFilters, + cellRenderer, + change, + columns, + currentParams, + compact, + editableCellValue, + editingCell, + editingCellSelectAll, + entities, + expandedEntityIdMap, + extraCompact, + filters, + getCellHoverText, + isCellEditable, + isEntityDisabled, + isLocalCall, + isSimple, + isSingleSelect, + isSelectionARectangle, + noDeselectAll, + noSelect, + noUserSelect, + onDeselect, + onMultiRowSelect, + onRowClick, + onRowSelect, + onSingleRowSelect, + order, + primarySelectedCellId, + reduxFormCellValidation, + reduxFormSelectedEntityIdMap, + refocusTable, + removeSingleFilter, + schema, + selectedCells, + setEditableCellValue, + setEditingCell, + setExpandedEntityIdMap, + setNewParams, + setOrder, + setSelectedCells, + shouldShowSubComponent, + startCellEdit, + SubComponent, + tableRef, + updateEntitiesHelper, + updateValidation, + withCheckboxes, + withExpandAndCollapseAllButton, + withFilter, + withSort + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + SubComponent, + addFilters, + cellRenderer, + columns, + compact, + currentParams, + editableCellValue, + editingCell, + editingCellSelectAll, + entities, + expandedEntityIdMap, + extraCompact, + filters, + getCellHoverText, + isCellEditable, + isEntityDisabled, + isLocalCall, + isSelectionARectangle, + isSimple, + isSingleSelect, + noDeselectAll, + noSelect, + noUserSelect, + onDeselect, + onMultiRowSelect, + onRowClick, + onRowSelect, + onSingleRowSelect, + order, + primarySelectedCellId, + reduxFormCellValidation, + reduxFormSelectedEntityIdMap, + removeSingleFilter, + schema, + selectedCells, + setNewParams, + setOrder, + shouldShowSubComponent, + startCellEdit, + updateEntitiesHelper, + updateValidation, + withCheckboxes, + withExpandAndCollapseAllButton, + withFilter, + withSort + ] + ); + + const reactTable = useMemo( + () => ( + { + return ; + }} + // We should try to not give all the props to the render column + columns={renderColumns} + pageSize={rowsToShow} + expanded={expandedRows} + showPagination={false} + sortable={false} + loading={isLoading || disabled} + defaultResized={resized} + onResizedChange={(newResized = []) => { + const resizedToUse = newResized.map(column => { + // have a min width of 50 so that columns don't disappear + if (column.value < 50) { + return { + ...column, + value: 50 + }; + } else { + return column; + } + }); + resizePersist(resizedToUse); + }} + TheadComponent={TheadComponent} + ThComponent={ThComponent} + getTrGroupProps={getTableRowProps} + getTdProps={getTableCellProps} + NoDataComponent={({ children }) => + isLoading ? null : ( +
{noRowsFoundMessage || children}
+ ) + } + LoadingComponent={props => ( + + )} + style={{ + maxHeight, + minHeight: 150, + ...style + }} + SubComponent={SubComponentToUse} + {...ReactTableProps} + /> + ), + [ + ReactTableProps, + SubComponentToUse, + TheadComponent, + compact, + disabled, + expandedRows, + extraCompact, + filteredEnts, + getTableCellProps, + getTableRowProps, + isCellEditable, + isLoading, + maxHeight, + noRowsFoundMessage, + renderColumns, + resizePersist, + resized, + rowsToShow, + style + ] + ); + return (
)} - { - return ; - }} - columns={renderColumns({ - ...props, - addFilters, - cellRenderer, - change, - columns, - currentParams, - compact, - editableCellValue, - editingCell, - entities, - expandedEntityIdMap, - extraCompact, - filters, - isCellEditable, - isEntityDisabled, - isLocalCall, - isSingleSelect, - isSelectionARectangle, - noDeselectAll, - noSelect, - onDeselect, - onMultiRowSelect, - onRowClick, - onRowSelect, - onSingleRowSelect, - primarySelectedCellId, - reduxFormCellValidation, - reduxFormSelectedEntityIdMap, - refocusTable, - schema, - selectedCells, - setEditableCellValue, - setEditingCell, - setExpandedEntityIdMap, - setSelectedCells, - shouldShowSubComponent, - startCellEdit, - SubComponent, - tableRef, - updateEntitiesHelper, - updateValidation, - withCheckboxes - })} - pageSize={rowsToShow} - expanded={expandedRows} - showPagination={false} - sortable={false} - loading={isLoading || disabled} - defaultResized={resized} - onResizedChange={(newResized = []) => { - const resizedToUse = newResized.map(column => { - // have a min width of 50 so that columns don't disappear - if (column.value < 50) { - return { - ...column, - value: 50 - }; - } else { - return column; - } - }); - resizePersist(resizedToUse); - }} - TheadComponent={TheadComponent} - ThComponent={ThComponent} - getTrGroupProps={getTableRowProps} - getTdProps={getTableCellProps} - NoDataComponent={({ children }) => - isLoading ? null : ( -
- {noRowsFoundMessage || children} -
- ) - } - LoadingComponent={props => ( - - )} - style={{ - maxHeight, - minHeight: 150, - ...style - }} - SubComponent={SubComponentToUse} - {...ReactTableProps} - /> + {reactTable} {isCellEditable && (
val && val.toLowerCase()) + .map(val => { + if (val) { + if (val.toString) return val.toString().toLowerCase(); + return val.toLowerCase(); + } + return undefined; + }) .indexOf(fieldVal.toString().toLowerCase()) > -1 ); }; @@ -377,7 +383,13 @@ function getSubFilter( if (!fieldVal?.toString) return false; return ( arrayFilterValue - .map(val => val && val.toLowerCase()) + .map(val => { + if (val) { + if (val.toString) return val.toString().toLowerCase(); + return val.toLowerCase(); + } + return undefined; + }) .indexOf(fieldVal.toString().toLowerCase()) === -1 ); }; diff --git a/packages/ui/src/DataTable/utils/useDeepEqualMemo.js b/packages/ui/src/DataTable/utils/useDeepEqualMemo.js new file mode 100644 index 00000000..72ac5c23 --- /dev/null +++ b/packages/ui/src/DataTable/utils/useDeepEqualMemo.js @@ -0,0 +1,10 @@ +import { isEqual } from "lodash-es"; +import { useRef } from "react"; + +export const useDeepEqualMemo = value => { + const ref = useRef(); + if (!isEqual(value, ref.current)) { + ref.current = value; + } + return ref.current; +}; diff --git a/packages/ui/src/DataTable/utils/withTableParams.js b/packages/ui/src/DataTable/utils/withTableParams.js index a5aab93c..f6385987 100644 --- a/packages/ui/src/DataTable/utils/withTableParams.js +++ b/packages/ui/src/DataTable/utils/withTableParams.js @@ -45,50 +45,30 @@ export default function withTableParams(compOrOpts, pTopLevelOpts) { const mapStateToProps = (state, ownProps) => { const mergedOpts = getMergedOpts(topLevelOptions, ownProps); const { - history, - urlConnected, - withSelectedEntities, - formName, - formNameFromWithTPCall, - syncDisplayOptionsToDb, + additionalFilter = {}, + additionalOrFilter = {}, + cellRenderer, defaults, - isInfinite, - isSimple, - withPaging, doNotCoercePageSize, + formName, + history, initialValues, - additionalFilter = {}, - additionalOrFilter = {}, + isCodeModel, + isInfinite, + isSimple, + model, + noForm, noOrderError, + syncDisplayOptionsToDb, + urlConnected, withDisplayOptions, - cellRenderer, - model, - isCodeModel, - noForm + withPaging, + withSelectedEntities } = mergedOpts; const schema = getSchema(mergedOpts); const convertedSchema = convertSchema(schema); - if (ownProps.isTableParamsConnected) { - if ( - formName && - formNameFromWithTPCall && - formName !== formNameFromWithTPCall - ) { - console.error( - `You passed a formName prop, ${formName} to a component that is already withTableParams() connected, formNameFromWithTableParamsCall: ${formNameFromWithTPCall}` - ); - } - if (ownProps.tableParams && !ownProps.tableParams.entities) { - console.error( - `No entities array detected in tableParams object (). You need to call withQuery() after withTableParams() like: compose(withTableParams(), withQuery(something)). formNameFromWithTableParamsCall: ${formNameFromWithTPCall}` - ); - } - //short circuit because we've already run this logic - return {}; - } - let formNameFromWithTableParamsCall; if (isLocalCall) { if (!noForm && (!formName || formName === "tgDataTable")) { console.error( @@ -110,8 +90,6 @@ export default function withTableParams(compOrOpts, pTopLevelOpts) { "Please pass a unique 'formName' prop to the withTableParams() with schema: ", schema ); - } else { - formNameFromWithTableParamsCall = formName; } } @@ -178,7 +156,6 @@ export default function withTableParams(compOrOpts, pTopLevelOpts) { isCodeModel, ownProps: mergedOpts }), - formNameFromWithTPCall: formNameFromWithTableParamsCall, currentParams, selectedEntities, ...(withSelectedEntities && diff --git a/packages/ui/src/FormComponents/Uploader.js b/packages/ui/src/FormComponents/Uploader.js index d2a498c6..003a6bc9 100644 --- a/packages/ui/src/FormComponents/Uploader.js +++ b/packages/ui/src/FormComponents/Uploader.js @@ -211,37 +211,37 @@ const InnerDropZone = ({ const Uploader = ({ accept: __accept, - contentOverride: maybeContentOverride, - innerIcon, - innerText, action, - className = "", - minimal, - validateAgainstSchema: _validateAgainstSchema, + autoUnzip, + beforeUpload, callout: _callout, + className = "", + contentOverride: maybeContentOverride, + disabled: _disabled, + dropzoneProps = {}, fileLimit, - readBeforeUpload, //read the file using the browser's FileReader before passing it to onChange and/or uploading it - showUploadList = true, - beforeUpload, fileList, //list of files with options: {name, loading, error, url, originalName, downloadName} + innerIcon, + innerText, + meta: { form: formName } = {}, + minimal, + name, + noBuildCsvOption, + noRedux = true, + onChange: _onChange = noop, //this is almost always getting passed by redux-form, no need to pass this handler manually + onFieldSubmit = noop, //called when all files have successfully uploaded + onFileClick, // called when a file link in the filelist is clicked onFileSuccess = async () => { return; }, //called each time a file is finished and before the file.loading gets set to false, needs to return a promise! - onFieldSubmit = noop, //called when all files have successfully uploaded - // fileFinished = noop, + onPreviewClick, onRemove = noop, //called when a file has been selected to be removed - onChange: _onChange = noop, //this is almost always getting passed by redux-form, no need to pass this handler manually - onFileClick, // called when a file link in the filelist is clicked - dropzoneProps = {}, overflowList, - autoUnzip, - disabled: _disabled, - noBuildCsvOption, + readBeforeUpload, //read the file using the browser's FileReader before passing it to onChange and/or uploading it showFilesCount, + showUploadList = true, threeDotMenuItems, - onPreviewClick, - noRedux = true, - meta: { form: formName } = {} + validateAgainstSchema: _validateAgainstSchema }) => { const dispatch = useDispatch(); const [acceptLoading, setAcceptLoading] = useState(); @@ -254,7 +254,7 @@ const Uploader = ({ if (noRedux) { return _onChange(val); } - return dispatch(change(formName, "exampleFile", val)); + return dispatch(change(formName, name, val)); }; const handleSecondHalfOfUpload = async ({ diff --git a/packages/ui/src/FormComponents/index.js b/packages/ui/src/FormComponents/index.js index e0d0f0db..83945f08 100644 --- a/packages/ui/src/FormComponents/index.js +++ b/packages/ui/src/FormComponents/index.js @@ -490,6 +490,7 @@ export const renderFileUpload = ({ input, onFieldSubmit, ...rest }) => ( fileList={input.value} onFieldSubmit={onFieldSubmit} {...rest} + name={input.name} onChange={input.onChange} /> ); diff --git a/packages/ui/src/MatchHeaders.js b/packages/ui/src/MatchHeaders.js index af7d68af..b727e940 100644 --- a/packages/ui/src/MatchHeaders.js +++ b/packages/ui/src/MatchHeaders.js @@ -111,7 +111,7 @@ export const MatchHeaders = ({ return rb - ra; }); return ( - + {row?.[userMatchedHeader] || ""} diff --git a/packages/ui/src/UploadCsvWizard.js b/packages/ui/src/UploadCsvWizard.js index f82e8f93..52e91123 100644 --- a/packages/ui/src/UploadCsvWizard.js +++ b/packages/ui/src/UploadCsvWizard.js @@ -17,7 +17,7 @@ import { some } from "lodash-es"; import { times } from "lodash-es"; import DialogFooter from "./DialogFooter"; import DataTable from "./DataTable"; -import { removeCleanRows } from "./DataTable/utils"; +import { removeCleanRows, useDeepEqualMemo } from "./DataTable/utils"; import wrapDialog from "./wrapDialog"; import { omit } from "lodash-es"; import { useDispatch, useSelector } from "react-redux"; @@ -179,15 +179,19 @@ export const SimpleInsertDataDialog = compose( validateAgainstSchema }) => { const dispatch = useDispatch(); - const reduxFormEntities = useSelector( + const _reduxFormEntities = useSelector( state => state.form?.[dataTableForm]?.values.reduxFormEntities ); + const reduxFormEntities = useDeepEqualMemo(_reduxFormEntities); useEffect(() => { return () => dispatch(destroy(dataTableForm)); }, [dataTableForm, dispatch]); - const reduxFormCellValidation = useSelector( + + const _reduxFormCellValidation = useSelector( state => state.form?.[dataTableForm]?.values.reduxFormCellValidation ); + const reduxFormCellValidation = useDeepEqualMemo(_reduxFormCellValidation); + const { entsToUse, validationToUse } = useMemo( () => removeCleanRows(reduxFormEntities, reduxFormCellValidation), [reduxFormEntities, reduxFormCellValidation] @@ -268,7 +272,10 @@ const UploadCsvWizardDialogInner = reduxForm()(({ const [hasSubmitted, setSubmitted] = useState(!csvValidationIssue); const [steps, setSteps] = useState(getInitialSteps(csvValidationIssue)); - const { reduxFormEntities, reduxFormCellValidation } = useSelector(state => + const { + reduxFormEntities: _reduxFormEntities, + reduxFormCellValidation: _reduxFormCellValidation + } = useSelector(state => formValueSelector(datatableFormName)( state, "reduxFormEntities", @@ -276,6 +283,9 @@ const UploadCsvWizardDialogInner = reduxForm()(({ ) ); + const reduxFormEntities = useDeepEqualMemo(_reduxFormEntities); + const reduxFormCellValidation = useDeepEqualMemo(_reduxFormCellValidation); + let inner; if (hasSubmitted) { inner = ( @@ -390,6 +400,220 @@ const UploadCsvWizardDialogInner = reduxForm()(({ ); }); +// // usefull +// const useTraceUpdate = props => { +// const prev = useRef(props); +// useEffect(() => { +// const changedProps = Object.entries(props).reduce((ps, [k, v]) => { +// if (prev.current[k] !== v) { +// ps[k] = [prev.current[k], v]; +// } +// return ps; +// }, {}); +// if (Object.keys(changedProps).length > 0) { +// console.log("Changed props:", changedProps); +// } +// prev.current = props; +// }); +// }; + +const MultipleFileDialog = ({ + focusedTab, + setFocusedTab, + filesWIssues, + finishedFiles, + doAllFilesHaveSameHeaders, + setSubmittedOuter, + setSteps, + reduxFormEntitiesArray, + validateAgainstSchema, + changeForm, + onUploadWizardFinish, + setFilesWIssues, + csvValidationIssue, + ignoredHeadersMsg, + searchResults, + matchedHeaders, + userSchema, + flippedMatchedHeaders, + steps, + hasSubmittedOuter +}) => { + const tabs = ( + <> + +
+ Please look over each of the following files and correct any issues. +
+
+ { + setFocusedTab(i); + }} + vertical + > + {filesWIssues.map((f, i) => { + const isGood = finishedFiles[i]; + const isThisTheLastBadFile = finishedFiles.every((ff, j) => { + if (i === j) { + return true; + } else { + return !!ff; + } + }); + return ( + + {" "} + {f.file.name} + + } + panel={ + { + setSubmittedOuter(false); + setSteps(getInitialSteps(true)); + }) + } + onMultiFileUploadSubmit={async () => { + let nextUnfinishedFile; + //find the next unfinished file + for ( + let j = (i + 1) % finishedFiles.length; + j < finishedFiles.length; + j++ + ) { + if (j === i) { + break; + } else if (!finishedFiles[j]) { + nextUnfinishedFile = j; + break; + } else if (j === finishedFiles.length - 1) { + j = -1; + } + } + if (nextUnfinishedFile !== undefined) { + //do async validation here if needed + const currentEnts = reduxFormEntitiesArray[focusedTab]; + if ( + await asyncValidateHelper( + validateAgainstSchema, + currentEnts, + changeForm, + `editableCellTable-${focusedTab}` + ) + ) + return; + setFocusedTab(nextUnfinishedFile); + } else { + //do async validation here if needed + for (const [i, ents] of finishedFiles.entries()) { + if ( + await asyncValidateHelper( + validateAgainstSchema, + ents, + changeForm, + `editableCellTable-${i}` + ) + ) + return; + } + //we are done + onUploadWizardFinish({ + res: finishedFiles.map(ents => { + return maybeStripIdFromEntities( + ents, + f.validateAgainstSchema + ); + }) + }); + } + }} + validateAgainstSchema={validateAgainstSchema} + reduxFormEntitiesArray={reduxFormEntitiesArray} + filesWIssues={filesWIssues} + finishedFiles={finishedFiles} + onUploadWizardFinish={onUploadWizardFinish} + doAllFilesHaveSameHeaders={doAllFilesHaveSameHeaders} + setFilesWIssues={setFilesWIssues} + csvValidationIssue={csvValidationIssue} + ignoredHeadersMsg={ignoredHeadersMsg} + searchResults={searchResults} + matchedHeader={matchedHeaders} + userSchema={userSchema} + flippedMatchedHeaders={flippedMatchedHeaders} + changeForm={changeForm} + fileIndex={i} + form={`correctCSVHeadersForm-${i}`} + datatableFormName={`editableCellTable-${i}`} + {...f} + {...(doAllFilesHaveSameHeaders && { + csvValidationIssue: false + })} + /> + } + /> + ); + })} + + + ); + let comp = tabs; + + if (doAllFilesHaveSameHeaders) { + comp = ( + <> + {doAllFilesHaveSameHeaders && ( + + )} + + {!hasSubmittedOuter && ( + { + return `editableCellTable-${i}`; + })} + reduxFormEntitiesArray={reduxFormEntitiesArray} + csvValidationIssue={csvValidationIssue} + ignoredHeadersMsg={ignoredHeadersMsg} + searchResults={searchResults} + matchedHeaders={matchedHeaders} + userSchema={userSchema} + flippedMatchedHeaders={flippedMatchedHeaders} + setFilesWIssues={setFilesWIssues} + filesWIssues={filesWIssues} + fileIndex={0} + {...filesWIssues[0]} + /> + )} + {hasSubmittedOuter && tabs} + {!hasSubmittedOuter && ( + { + setSubmittedOuter(true); + setSteps(getInitialSteps(false)); + }} + text="Review and Edit Data" + /> + )} + + ); + } + return
{comp}
; +}; + const UploadCsvWizardDialog = compose( wrapDialog({ canEscapeKeyClose: false, @@ -427,7 +651,7 @@ const UploadCsvWizardDialog = compose( }; }, [_filesWIssues.length, dispatch]); - const { reduxFormEntitiesArray, finishedFiles } = useSelector(state => { + const { _reduxFormEntitiesArray, _finishedFiles } = useSelector(state => { if (_filesWIssues.length > 0) { const reduxFormEntitiesArray = []; const finishedFiles = _filesWIssues.map((f, i) => { @@ -455,6 +679,8 @@ const UploadCsvWizardDialog = compose( }; } }); + const reduxFormEntitiesArray = useDeepEqualMemo(_reduxFormEntitiesArray); + const finishedFiles = useDeepEqualMemo(_finishedFiles); const [hasSubmittedOuter, setSubmittedOuter] = useState(); const [steps, setSteps] = useState(getInitialSteps(true)); @@ -463,180 +689,32 @@ const UploadCsvWizardDialog = compose( const [filesWIssues, setFilesWIssues] = useState( _filesWIssues.map(cloneDeep) //do this little trick to stop immer from preventing the file from being modified ); + if (filesWIssues.length > 1) { - const tabs = ( - <> - -
- Please look over each of the following files and correct any issues. -
-
- { - setFocusedTab(i); - }} - vertical - > - {filesWIssues.map((f, i) => { - const isGood = finishedFiles[i]; - const isThisTheLastBadFile = finishedFiles.every((ff, j) => { - if (i === j) { - return true; - } else { - return !!ff; - } - }); - return ( - - {" "} - {f.file.name} - - } - panel={ - { - setSubmittedOuter(false); - setSteps(getInitialSteps(true)); - }) - } - onMultiFileUploadSubmit={async () => { - let nextUnfinishedFile; - //find the next unfinished file - for ( - let j = (i + 1) % finishedFiles.length; - j < finishedFiles.length; - j++ - ) { - if (j === i) { - break; - } else if (!finishedFiles[j]) { - nextUnfinishedFile = j; - break; - } else if (j === finishedFiles.length - 1) { - j = -1; - } - } - if (nextUnfinishedFile !== undefined) { - //do async validation here if needed - const currentEnts = reduxFormEntitiesArray[focusedTab]; - if ( - await asyncValidateHelper( - validateAgainstSchema, - currentEnts, - changeForm, - `editableCellTable-${focusedTab}` - ) - ) - return; - setFocusedTab(nextUnfinishedFile); - } else { - //do async validation here if needed - for (const [i, ents] of finishedFiles.entries()) { - if ( - await asyncValidateHelper( - validateAgainstSchema, - ents, - changeForm, - `editableCellTable-${i}` - ) - ) - return; - } - //we are done - onUploadWizardFinish({ - res: finishedFiles.map(ents => { - return maybeStripIdFromEntities( - ents, - f.validateAgainstSchema - ); - }) - }); - } - }} - validateAgainstSchema={validateAgainstSchema} - reduxFormEntitiesArray={reduxFormEntitiesArray} - filesWIssues={filesWIssues} - finishedFiles={finishedFiles} - onUploadWizardFinish={onUploadWizardFinish} - doAllFilesHaveSameHeaders={doAllFilesHaveSameHeaders} - setFilesWIssues={setFilesWIssues} - csvValidationIssue={csvValidationIssue} - ignoredHeadersMsg={ignoredHeadersMsg} - searchResults={searchResults} - matchedHeader={matchedHeaders} - userSchema={userSchema} - flippedMatchedHeaders={flippedMatchedHeaders} - changeForm={changeForm} - fileIndex={i} - form={`correctCSVHeadersForm-${i}`} - datatableFormName={`editableCellTable-${i}`} - {...f} - {...(doAllFilesHaveSameHeaders && { - csvValidationIssue: false - })} - /> - } - /> - ); - })} - - + return ( + ); - let comp = tabs; - - if (doAllFilesHaveSameHeaders) { - comp = ( - <> - {doAllFilesHaveSameHeaders && ( - - )} - - {!hasSubmittedOuter && ( - { - return `editableCellTable-${i}`; - })} - reduxFormEntitiesArray={reduxFormEntitiesArray} - csvValidationIssue={csvValidationIssue} - ignoredHeadersMsg={ignoredHeadersMsg} - searchResults={searchResults} - matchedHeaders={matchedHeaders} - userSchema={userSchema} - flippedMatchedHeaders={flippedMatchedHeaders} - setFilesWIssues={setFilesWIssues} - filesWIssues={filesWIssues} - fileIndex={0} - {...filesWIssues[0]} - /> - )} - {hasSubmittedOuter && tabs} - {!hasSubmittedOuter && ( - { - setSubmittedOuter(true); - setSteps(getInitialSteps(false)); - }} - text="Review and Edit Data" - /> - )} - - ); - } - return
{comp}
; } else { return ( @@ -13,32 +12,36 @@ import React, { useState } from "react"; */ -export const useDialog = ({ ModalComponent, ...rest }) => { +/* + This useDialog is producing many rerenders for unknown reasons with cypress +*/ +export const useDialog = ({ ModalComponent }) => { const [isOpen, setOpen] = useState(false); const [additionalProps, setAdditionalProps] = useState(false); - const comp = ( - { - setOpen(false); - }} - hideDialog={() => { - setOpen(false); - }} - {...rest} - {...additionalProps} - dialogProps={{ - isOpen, - ...rest?.dialogProps, - ...additionalProps?.dialogProps - }} - /> + const comp = useMemo( + () => ( + { + setOpen(false); + }} + hideDialog={() => { + setOpen(false); + }} + {...additionalProps} + dialogProps={{ + isOpen, + ...additionalProps?.dialogProps + }} + /> + ), + [ModalComponent, additionalProps, isOpen] ); const toggleDialog = () => { - setOpen(!isOpen); + setOpen(prev => !prev); }; - const showDialogPromise = async (handlerName, moreProps = {}) => { + const showDialogPromise = useCallback(async (handlerName, moreProps = {}) => { return new Promise(resolve => { //return a promise that can be awaited setAdditionalProps({ @@ -61,7 +64,7 @@ export const useDialog = ({ ModalComponent, ...rest }) => { }); setOpen(true); //open the dialog }); - }; + }, []); return { comp, showDialogPromise, toggleDialog, setAdditionalProps }; };