From 8736121def03af1341d6477e64075514c9b41507 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Tue, 17 Dec 2024 20:39:44 -0700 Subject: [PATCH] Inline editing issues Inline editing while filters were applied would cause edited values to be removed from the table and would remain hidden even after resetting all filters. This was caused because the SET filter did not update to include the new value, so the record would be filtered out from the table - and even after resetting, the filter values were not re-calculated. In addition the onRecordChanged emits the filtered values, not all values, so the parent component was saving just the filtered records thinking that was the universe - this also caused some (most?) records to not be available to be saved when submitting to Salesforce. To solve, the data table component intercepts the change and ensures that the table set filters are updated to include the new value in the list and select the new record column value. Unfortunately this only solves the set filter and does not solve any other filters (text, date, etc..) - that would require a complete refactor of how state is managed for the table, which is high-risk and a lot of work - so we will deal with it for now. To help this, a number has been added to the submit button to show the number of changed records. resolves #1113 --- libs/ui/src/lib/data-table/DataTable.tsx | 7 ++ .../data-table/SalesforceRecordDataTable.tsx | 30 ++++--- libs/ui/src/lib/data-table/useDataTable.tsx | 82 ++++++++++++++++++- 3 files changed, 105 insertions(+), 14 deletions(-) diff --git a/libs/ui/src/lib/data-table/DataTable.tsx b/libs/ui/src/lib/data-table/DataTable.tsx index af6d0d39..b75b5a25 100644 --- a/libs/ui/src/lib/data-table/DataTable.tsx +++ b/libs/ui/src/lib/data-table/DataTable.tsx @@ -83,6 +83,7 @@ export const DataTable = forwardRef>( handleCellKeydown, handleCellContextMenu, handleCloseContextMenu, + handleRowChange, } = useDataTable({ data, columns: _columns, @@ -130,6 +131,12 @@ export const DataTable = forwardRef>( // @ts-expect-error Types are incorrect, but they are generic and difficult to get correct onCellContextMenu={handleCellContextMenu} {...rest} + onRowsChange={(rows, data) => { + if (rest.onRowsChange) { + handleRowChange(rows, data); + rest.onRowsChange(rows as any, data as any); + } + }} /> {contextMenuProps && contextMenuItems && contextMenuAction && ( ) => { - setRows(rows); - setDirtyRows( - rows.filter((row) => row._touchedColumns.size > 0 && Array.from(row._touchedColumns).some((col) => row[col] !== row._record[col])) - ); - }, []); + const handleRowsChange = useCallback( + ( + allRows: RowSalesforceRecordWithKey[], + filteredRows: RowSalesforceRecordWithKey[], + data: RowsChangeData + ) => { + const rowsByKey = groupByFlat(filteredRows, '_key'); + const newRows = allRows.map((row) => rowsByKey[row._key] || row); + setRows(newRows); + setDirtyRows( + newRows.filter( + (row) => row._touchedColumns.size > 0 && Array.from(row._touchedColumns).some((col) => row[col] !== row._record[col]) + ) + ); + }, + [] + ); const handleCancelEditMode = () => { setRecords((records) => (records ? [...records] : records)); @@ -435,8 +446,7 @@ export const SalesforceRecordDataTable: FunctionComponent - Save - {isSavingRecords && } + Save ({formatNumber(dirtyRows.length)}){isSavingRecords && } )} @@ -469,7 +479,7 @@ export const SalesforceRecordDataTable: FunctionComponent handleRowsChange(rows || [], changedRows, data)} context={{ org, defaultApiVersion, diff --git a/libs/ui/src/lib/data-table/useDataTable.tsx b/libs/ui/src/lib/data-table/useDataTable.tsx index 57cc5fcb..caa047d9 100644 --- a/libs/ui/src/lib/data-table/useDataTable.tsx +++ b/libs/ui/src/lib/data-table/useDataTable.tsx @@ -17,6 +17,7 @@ import { CellMouseEvent, RenderSortStatusProps, Renderers, + RowsChangeData, SortColumn, } from 'react-data-grid'; import 'react-data-grid/lib/styles.css'; @@ -246,6 +247,10 @@ export function useDataTable({ [filteredRows] ); + const handleRowChange = useCallback((rows: any[], data: RowsChangeData) => { + dispatch({ type: 'ADD_MODIFIED_VALUE_TO_SET_FILTER', payload: { rows, data } }); + }, []); + const handleCloseContextMenu = useCallback(() => setContextMenuProps(null), []); // NOTE: this is not used anywhere, so we may consider removing it. @@ -288,6 +293,7 @@ export function useDataTable({ handleCellKeydown, handleCellContextMenu: contextMenuItems && contextMenuAction ? handleCellContextMenu : undefined, handleCloseContextMenu: handleCloseContextMenu, + handleRowChange, }; } @@ -314,6 +320,7 @@ interface State { type Action = | { type: 'INIT'; payload: { columns: ColumnWithFilter[]; data: any[]; ignoreRowInSetFilter?: (row: any) => boolean } } + | { type: 'ADD_MODIFIED_VALUE_TO_SET_FILTER'; payload: { rows: any[]; data: RowsChangeData } } | { type: 'UPDATE_FILTER'; payload: { column: string; filter: DataTableFilter } }; // Reducer is used to limit the number of re-renders because of dependent state @@ -376,16 +383,83 @@ function reducer(state: State, action: Action): State { filterSetValues, }; } - case 'UPDATE_FILTER': { - const { column, filter } = action.payload; + case 'ADD_MODIFIED_VALUE_TO_SET_FILTER': { + const { + data: { column, indexes }, + rows, + } = action.payload; + if (!state.filters[column.key]) { + return state; + } + const newValues = indexes.map((index) => { + const value = rows[index][column.key]; + if (value === '' || value === null) { + return EMPTY_FIELD; + } + return value; + }); + // NOTE: we don't have access to every record here, so we just add the values and don't worry about removing on subsequent record change + // Calculate new list of available values + const filterSetValues = { + ...state.filterSetValues, + [column.key]: Array.from(new Set([...state.filterSetValues[column.key], ...newValues])), + }; + // ensure that current values are included in the set filter so they are retained on the page while editing + const columnFilter = [...state.filters[column.key]].map((item) => { + if (item.type !== 'SET') { + return item; + } + return { + ...item, + value: Array.from(new Set(item.value.concat(newValues))), + }; + }); return { ...state, - hasFilters: true, + filterSetValues, filters: { ...state.filters, - [column]: state.filters[column].map((currFilter) => (currFilter.type === filter.type ? filter : currFilter)), + [column.key]: columnFilter, }, }; } + case 'UPDATE_FILTER': { + const { column, filter } = action.payload; + const filters = { + ...state.filters, + [column]: state.filters[column].map((currFilter) => (currFilter.type === filter.type ? filter : currFilter)), + }; + const hasFilters = hasFilterApplied(filters, state.filterSetValues); + return { + ...state, + hasFilters, + filters, + }; + } } } + +function hasFilterApplied(filters: Record, filterSetValues: Record) { + return Object.entries(filters).some(([key, columnFilters]) => + columnFilters.some((filter) => { + let applied = false; + switch (filter.type) { + case 'SET': + applied = filter.value.length < (filterSetValues[key]?.length || 0); + break; + case 'BOOLEAN_SET': + applied = filter.value.length < 2; // true/false + break; + case 'DATE': + case 'NUMBER': + case 'TEXT': + case 'TIME': + applied = !!filter.value; + break; + default: + return false; + } + return applied; + }) + ); +}