Skip to content

Commit a7c4e8d

Browse files
fix(data-grid): persist cell updates when filtering is applied (#1116)
* fix(data-grid): persist cell updates when filtering is applied Fixes issue where cell edits in data-grid-live were not persisted to the database when filters were active. The problem was that onDataUpdate was building a newData array with only the filtered row count instead of the complete dataset. When filters are applied: - The table's row model contains only filtered rows - But the data prop contains all rows (unfiltered) - onDataUpdate needs to return all rows with updates applied This fix ensures that when building the updated data array, we iterate over the complete currentData array (all rows) rather than just the visible filtered rows (tableRowCount). This allows onDataChange to properly detect and persist all updates to the database. Fixes #1106 Co-authored-by: Cursor <cursoragent@cursor.com> * test(data-grid): add tests for cell updates with filtering applied Added comprehensive tests to verify that cell updates work correctly when column filters are active: 1. Single filtered row update: Verifies that updating a cell in a filtered view correctly updates the full dataset (all 3 rows) rather than just the filtered rows 2. Multiple filtered rows update: Tests updating a cell when multiple rows are visible through filtering, ensuring the correct row in the full dataset is updated These tests ensure the fix for #1106 is working correctly and prevent regression of the filtering + cell update bug. All 96 tests passing. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: rebuild registry --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent aa82c57 commit a7c4e8d

5 files changed

Lines changed: 149 additions & 16 deletions

File tree

public/r/data-grid-filter-menu.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
},
5454
{
5555
"path": "src/types/data-grid.ts",
56-
"content": "import type { Cell, RowData, TableMeta } from \"@tanstack/react-table\";\n\nexport type Direction = \"ltr\" | \"rtl\";\n\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\n\nexport interface CellSelectOption {\n label: string;\n value: string;\n icon?: React.FC<React.SVGProps<SVGSVGElement>>;\n count?: number;\n}\n\nexport type CellOpts =\n | {\n variant: \"short-text\";\n }\n | {\n variant: \"long-text\";\n }\n | {\n variant: \"number\";\n min?: number;\n max?: number;\n step?: number;\n }\n | {\n variant: \"select\";\n options: CellSelectOption[];\n }\n | {\n variant: \"multi-select\";\n options: CellSelectOption[];\n }\n | {\n variant: \"checkbox\";\n }\n | {\n variant: \"date\";\n }\n | {\n variant: \"url\";\n }\n | {\n variant: \"file\";\n maxFileSize?: number;\n maxFiles?: number;\n accept?: string;\n multiple?: boolean;\n };\n\nexport interface CellUpdate {\n rowIndex: number;\n columnId: string;\n value: unknown;\n}\n\ndeclare module \"@tanstack/react-table\" {\n // biome-ignore lint/correctness/noUnusedVariables: TData and TValue are used in the ColumnMeta interface\n interface ColumnMeta<TData extends RowData, TValue> {\n label?: string;\n cell?: CellOpts;\n }\n\n // biome-ignore lint/correctness/noUnusedVariables: TData is used in the TableMeta interface\n interface TableMeta<TData extends RowData> {\n dataGridRef?: React.RefObject<HTMLElement | null>;\n cellMapRef?: React.RefObject<Map<string, HTMLDivElement>>;\n focusedCell?: CellPosition | null;\n editingCell?: CellPosition | null;\n selectionState?: SelectionState;\n searchOpen?: boolean;\n getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\n getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getVisualRowIndex?: (rowId: string) => number | undefined;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (value: RowHeightValue) => void;\n onRowSelect?: (\n rowIndex: number,\n checked: boolean,\n shiftKey: boolean,\n ) => void;\n onDataUpdate?: (params: CellUpdate | Array<CellUpdate>) => void;\n onRowsDelete?: (rowIndices: number[]) => void | Promise<void>;\n onColumnClick?: (columnId: string) => void;\n onCellClick?: (\n rowIndex: number,\n columnId: string,\n event?: React.MouseEvent,\n ) => void;\n onCellDoubleClick?: (rowIndex: number, columnId: string) => void;\n onCellMouseDown?: (\n rowIndex: number,\n columnId: string,\n event: React.MouseEvent,\n ) => void;\n onCellMouseEnter?: (rowIndex: number, columnId: string) => void;\n onCellMouseUp?: () => void;\n onCellContextMenu?: (\n rowIndex: number,\n columnId: string,\n event: React.MouseEvent,\n ) => void;\n onCellEditingStart?: (rowIndex: number, columnId: string) => void;\n onCellEditingStop?: (opts?: {\n direction?: NavigationDirection;\n moveToNextRow?: boolean;\n }) => void;\n onCellsCopy?: () => void;\n onCellsCut?: () => void;\n onCellsPaste?: (expand?: boolean) => void;\n onSelectionClear?: () => void;\n onFilesUpload?: (params: {\n files: File[];\n rowIndex: number;\n columnId: string;\n }) => Promise<FileCellData[]>;\n onFilesDelete?: (params: {\n fileIds: string[];\n rowIndex: number;\n columnId: string;\n }) => void | Promise<void>;\n contextMenu?: ContextMenuState;\n onContextMenuOpenChange?: (open: boolean) => void;\n pasteDialog?: PasteDialogState;\n onPasteDialogOpenChange?: (open: boolean) => void;\n readOnly?: boolean;\n }\n}\n\nexport interface CellPosition {\n rowIndex: number;\n columnId: string;\n}\n\nexport interface CellRange {\n start: CellPosition;\n end: CellPosition;\n}\n\nexport interface SelectionState {\n selectedCells: Set<string>;\n selectionRange: CellRange | null;\n isSelecting: boolean;\n}\n\nexport interface ContextMenuState {\n open: boolean;\n x: number;\n y: number;\n}\n\nexport interface PasteDialogState {\n open: boolean;\n rowsNeeded: number;\n clipboardText: string;\n}\n\nexport type NavigationDirection =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"home\"\n | \"end\"\n | \"ctrl+up\"\n | \"ctrl+down\"\n | \"ctrl+home\"\n | \"ctrl+end\"\n | \"pageup\"\n | \"pagedown\"\n | \"pageleft\"\n | \"pageright\";\n\nexport interface SearchState {\n searchMatches: CellPosition[];\n matchIndex: number;\n searchOpen: boolean;\n onSearchOpenChange: (open: boolean) => void;\n searchQuery: string;\n onSearchQueryChange: (query: string) => void;\n onSearch: (query: string) => void;\n onNavigateToNextMatch: () => void;\n onNavigateToPrevMatch: () => void;\n}\n\nexport interface DataGridCellProps<TData> {\n cell: Cell<TData, unknown>;\n tableMeta: TableMeta<TData>;\n rowIndex: number;\n columnId: string;\n rowHeight: RowHeightValue;\n isEditing: boolean;\n isFocused: boolean;\n isSelected: boolean;\n isSearchMatch: boolean;\n isActiveSearchMatch: boolean;\n readOnly: boolean;\n}\n\nexport interface FileCellData {\n id: string;\n name: string;\n size: number;\n type: string;\n url?: string;\n}\n\nexport type TextFilterOperator =\n | \"contains\"\n | \"notContains\"\n | \"equals\"\n | \"notEquals\"\n | \"startsWith\"\n | \"endsWith\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type NumberFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"lessThan\"\n | \"lessThanOrEqual\"\n | \"greaterThan\"\n | \"greaterThanOrEqual\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type DateFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"before\"\n | \"after\"\n | \"onOrBefore\"\n | \"onOrAfter\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type SelectFilterOperator =\n | \"is\"\n | \"isNot\"\n | \"isAnyOf\"\n | \"isNoneOf\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type BooleanFilterOperator = \"isTrue\" | \"isFalse\";\n\nexport type FilterOperator =\n | TextFilterOperator\n | NumberFilterOperator\n | DateFilterOperator\n | SelectFilterOperator\n | BooleanFilterOperator;\n\nexport interface FilterValue {\n operator: FilterOperator;\n value?: string | number | string[];\n endValue?: string | number;\n}\n",
56+
"content": "import type { Cell, RowData, TableMeta } from \"@tanstack/react-table\";\n\nexport type Direction = \"ltr\" | \"rtl\";\n\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\n\nexport interface CellSelectOption {\n label: string;\n value: string;\n icon?: React.FC<React.SVGProps<SVGSVGElement>>;\n count?: number;\n}\n\nexport type CellOpts =\n | {\n variant: \"short-text\";\n }\n | {\n variant: \"long-text\";\n }\n | {\n variant: \"number\";\n min?: number;\n max?: number;\n step?: number;\n }\n | {\n variant: \"select\";\n options: CellSelectOption[];\n }\n | {\n variant: \"multi-select\";\n options: CellSelectOption[];\n }\n | {\n variant: \"checkbox\";\n }\n | {\n variant: \"date\";\n }\n | {\n variant: \"url\";\n }\n | {\n variant: \"file\";\n maxFileSize?: number;\n maxFiles?: number;\n accept?: string;\n multiple?: boolean;\n };\n\nexport interface CellUpdate {\n rowIndex: number;\n columnId: string;\n value: unknown;\n}\n\ndeclare module \"@tanstack/react-table\" {\n // biome-ignore lint/correctness/noUnusedVariables: TData and TValue are used in the ColumnMeta interface\n interface ColumnMeta<TData extends RowData, TValue> {\n label?: string;\n cell?: CellOpts;\n }\n\n // biome-ignore lint/correctness/noUnusedVariables: TData is used in the TableMeta interface\n interface TableMeta<TData extends RowData> {\n dataGridRef?: React.RefObject<HTMLElement | null>;\n cellMapRef?: React.RefObject<Map<string, HTMLDivElement>>;\n focusedCell?: CellPosition | null;\n editingCell?: CellPosition | null;\n selectionState?: SelectionState;\n searchOpen?: boolean;\n getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\n getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getVisualRowIndex?: (rowId: string) => number | undefined;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (value: RowHeightValue) => void;\n onRowSelect?: (rowId: string, checked: boolean, shiftKey: boolean) => void;\n onDataUpdate?: (params: CellUpdate | Array<CellUpdate>) => void;\n onRowsDelete?: (rowIndices: number[]) => void | Promise<void>;\n onColumnClick?: (columnId: string) => void;\n onCellClick?: (\n rowIndex: number,\n columnId: string,\n event?: React.MouseEvent,\n ) => void;\n onCellDoubleClick?: (rowIndex: number, columnId: string) => void;\n onCellMouseDown?: (\n rowIndex: number,\n columnId: string,\n event: React.MouseEvent,\n ) => void;\n onCellMouseEnter?: (rowIndex: number, columnId: string) => void;\n onCellMouseUp?: () => void;\n onCellContextMenu?: (\n rowIndex: number,\n columnId: string,\n event: React.MouseEvent,\n ) => void;\n onCellEditingStart?: (rowIndex: number, columnId: string) => void;\n onCellEditingStop?: (opts?: {\n direction?: NavigationDirection;\n moveToNextRow?: boolean;\n }) => void;\n onCellsCopy?: () => void;\n onCellsCut?: () => void;\n onCellsPaste?: (expand?: boolean) => void;\n onSelectionClear?: () => void;\n onFilesUpload?: (params: {\n files: File[];\n rowIndex: number;\n columnId: string;\n }) => Promise<FileCellData[]>;\n onFilesDelete?: (params: {\n fileIds: string[];\n rowIndex: number;\n columnId: string;\n }) => void | Promise<void>;\n contextMenu?: ContextMenuState;\n onContextMenuOpenChange?: (open: boolean) => void;\n pasteDialog?: PasteDialogState;\n onPasteDialogOpenChange?: (open: boolean) => void;\n readOnly?: boolean;\n }\n}\n\nexport interface CellPosition {\n rowIndex: number;\n columnId: string;\n}\n\nexport interface CellRange {\n start: CellPosition;\n end: CellPosition;\n}\n\nexport interface SelectionState {\n selectedCells: Set<string>;\n selectionRange: CellRange | null;\n isSelecting: boolean;\n}\n\nexport interface ContextMenuState {\n open: boolean;\n x: number;\n y: number;\n}\n\nexport interface PasteDialogState {\n open: boolean;\n rowsNeeded: number;\n clipboardText: string;\n}\n\nexport type NavigationDirection =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"home\"\n | \"end\"\n | \"ctrl+up\"\n | \"ctrl+down\"\n | \"ctrl+home\"\n | \"ctrl+end\"\n | \"pageup\"\n | \"pagedown\"\n | \"pageleft\"\n | \"pageright\";\n\nexport interface SearchState {\n searchMatches: CellPosition[];\n matchIndex: number;\n searchOpen: boolean;\n onSearchOpenChange: (open: boolean) => void;\n searchQuery: string;\n onSearchQueryChange: (query: string) => void;\n onSearch: (query: string) => void;\n onNavigateToNextMatch: () => void;\n onNavigateToPrevMatch: () => void;\n}\n\nexport interface DataGridCellProps<TData> {\n cell: Cell<TData, unknown>;\n tableMeta: TableMeta<TData>;\n rowIndex: number;\n columnId: string;\n rowHeight: RowHeightValue;\n isEditing: boolean;\n isFocused: boolean;\n isSelected: boolean;\n isSearchMatch: boolean;\n isActiveSearchMatch: boolean;\n readOnly: boolean;\n}\n\nexport interface FileCellData {\n id: string;\n name: string;\n size: number;\n type: string;\n url?: string;\n}\n\nexport type TextFilterOperator =\n | \"contains\"\n | \"notContains\"\n | \"equals\"\n | \"notEquals\"\n | \"startsWith\"\n | \"endsWith\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type NumberFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"lessThan\"\n | \"lessThanOrEqual\"\n | \"greaterThan\"\n | \"greaterThanOrEqual\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type DateFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"before\"\n | \"after\"\n | \"onOrBefore\"\n | \"onOrAfter\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type SelectFilterOperator =\n | \"is\"\n | \"isNot\"\n | \"isAnyOf\"\n | \"isNoneOf\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type BooleanFilterOperator = \"isTrue\" | \"isFalse\";\n\nexport type FilterOperator =\n | TextFilterOperator\n | NumberFilterOperator\n | DateFilterOperator\n | SelectFilterOperator\n | BooleanFilterOperator;\n\nexport interface FilterValue {\n operator: FilterOperator;\n value?: string | number | string[];\n endValue?: string | number;\n}\n",
5757
"type": "registry:file",
5858
"target": "src/types/data-grid.ts"
5959
}

0 commit comments

Comments
 (0)