@svgrid/grid 3.0.5 → 3.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -7
- package/dist/GridMenus.svelte +34 -4
- package/dist/SvGrid.controller.svelte.d.ts +21 -3
- package/dist/SvGrid.controller.svelte.js +205 -50
- package/dist/SvGrid.css +127 -2
- package/dist/SvGrid.svelte +223 -38
- package/dist/SvGrid.types.d.ts +77 -4
- package/dist/SvGridDropdown.svelte +20 -0
- package/dist/SvGridDropdown.svelte.d.ts +12 -0
- package/dist/SvModal.svelte +22 -0
- package/dist/SvModal.svelte.d.ts +7 -0
- package/dist/a11y/dismissable.d.ts +10 -0
- package/dist/a11y/dismissable.js +24 -2
- package/dist/build-api.js +81 -31
- package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-C-NtjlrJ.js} +150 -141
- package/dist/cdn/GridMenus-TssHej3q.js +644 -0
- package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
- package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-BlyCSp0q.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-ClRRA9lW.js} +1 -1
- package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-B4EV8U6t.js} +4 -4
- package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DsmwTyZF.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-cHloHobN.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-vOl7C0Za.js} +4 -4
- package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-AcqF-Ll5.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-biMtSBPA.js} +5 -5
- package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-Dx_GmZuL.js} +2 -2
- package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-I3rDHXAq.js} +2 -2
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
- package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
- package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
- package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-DVVKOSI9.js} +1 -1
- package/dist/cdn/{chart-Dz7SqMXH.js → chart-DEZxwC0i.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-D12XcTZJ.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DguS7gC5.js} +1 -1
- package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-CMAXBGY-.js} +1 -1
- package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-BkEp61Gj.js} +1 -1
- package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
- package/dist/cdn/{export-format-CUDLy2yn.js → export-format-YzdwBww2.js} +1 -1
- package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
- package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
- package/dist/cdn/{src-jcFMkBnN.js → src-CgoeSM8k.js} +4697 -4131
- package/dist/cdn/{src-Bklofsad.js → src-DfwxB1TH.js} +4727 -4161
- package/dist/cdn/svgrid.js +17 -17
- package/dist/cdn/svgrid.svelte-external.js +17 -17
- package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
- package/dist/cell-formatting.js +18 -1
- package/dist/cell-render.js +25 -6
- package/dist/clipboard.d.ts +2 -2
- package/dist/clipboard.js +72 -23
- package/dist/columns.js +7 -2
- package/dist/conditional-formatting.d.ts +2 -0
- package/dist/conditional-formatting.js +2 -0
- package/dist/core.d.ts +22 -6
- package/dist/core.js +365 -25
- package/dist/editing.js +24 -0
- package/dist/grid-messages.d.ts +4 -0
- package/dist/grid-messages.js +4 -0
- package/dist/headless.d.ts +5 -0
- package/dist/headless.js +12 -0
- package/dist/history.d.ts +21 -0
- package/dist/history.js +34 -4
- package/dist/index.d.ts +1 -1
- package/dist/keyboard-handlers.js +24 -1
- package/dist/keyboard.js +4 -0
- package/dist/row-drag-touch.d.ts +1 -1
- package/dist/row-drag.js +35 -1
- package/dist/row-model.d.ts +6 -0
- package/dist/row-resize.js +22 -1
- package/dist/selection.js +1 -1
- package/dist/server-block-cache.d.ts +5 -0
- package/dist/server-block-cache.js +8 -0
- package/dist/server-data-source.d.ts +35 -3
- package/dist/server-data-source.js +11 -2
- package/dist/server.d.ts +1 -1
- package/dist/sparkline.js +7 -5
- package/dist/svgrid-wrapper.types.d.ts +33 -0
- package/dist/validate.js +9 -1
- package/package.json +6 -1
- package/src/GridMenus.svelte +34 -4
- package/src/SvGrid.controller.svelte.ts +206 -53
- package/src/SvGrid.css +127 -2
- package/src/SvGrid.svelte +223 -38
- package/src/SvGrid.types.ts +77 -4
- package/src/SvGridDropdown.svelte +20 -0
- package/src/SvModal.svelte +22 -0
- package/src/SvModal.test.ts +30 -1
- package/src/a11y/dismissable.test.ts +10 -1
- package/src/a11y/dismissable.ts +22 -2
- package/src/build-api.ts +85 -27
- package/src/cell-formatting.test.ts +17 -0
- package/src/cell-formatting.ts +18 -1
- package/src/cell-render.test.ts +24 -2
- package/src/cell-render.ts +24 -8
- package/src/clipboard.test.ts +107 -0
- package/src/clipboard.ts +62 -23
- package/src/columns.test.ts +4 -4
- package/src/columns.ts +7 -2
- package/src/conditional-formatting.test.ts +8 -0
- package/src/conditional-formatting.ts +3 -0
- package/src/core.tick-repair.test.ts +287 -0
- package/src/core.ts +374 -26
- package/src/editing.test.ts +52 -0
- package/src/editing.ts +25 -0
- package/src/grid-messages.test.ts +7 -0
- package/src/grid-messages.ts +10 -0
- package/src/headless.ts +24 -0
- package/src/history.test.ts +49 -1
- package/src/history.ts +48 -2
- package/src/index.ts +2 -0
- package/src/keyboard-handlers.coverage.test.ts +65 -0
- package/src/keyboard-handlers.ts +26 -1
- package/src/keyboard.ts +3 -0
- package/src/row-drag-touch.ts +2 -2
- package/src/row-drag.test.ts +51 -0
- package/src/row-drag.ts +35 -2
- package/src/row-model.ts +6 -0
- package/src/row-resize.ts +24 -1
- package/src/selection.ts +1 -1
- package/src/server-block-cache.test.ts +27 -0
- package/src/server-block-cache.ts +13 -0
- package/src/server-data-source.infinite.test.ts +62 -0
- package/src/server-data-source.test.ts +19 -0
- package/src/server-data-source.ts +48 -4
- package/src/server.ts +2 -0
- package/src/sparkline.test.ts +7 -0
- package/src/sparkline.ts +6 -4
- package/src/svgrid-wrapper.types.ts +32 -0
- package/src/svgrid.api.test.ts +21 -0
- package/src/svgrid.charting.test.ts +12 -4
- package/src/svgrid.context-menu.test.ts +12 -0
- package/src/svgrid.detail-rows.svelte.test.ts +187 -0
- package/src/svgrid.filter-menu-scroll.test.ts +9 -0
- package/src/svgrid.gantt-seam.test.ts +1 -1
- package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
- package/src/svgrid.menu-scroll-close.test.ts +10 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
- package/src/svgrid.row-pinning.test.ts +15 -1
- package/src/svgrid.selection-bar-seam.test.ts +1 -1
- package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
- package/src/svgrid.upsell-license.test.ts +1 -1
- package/src/test-setup.ts +12 -0
- package/src/transaction.test.ts +58 -0
- package/src/validate.ts +7 -1
- package/dist/cdn/GridMenus-DLL-qZeu.js +0 -635
package/src/core.ts
CHANGED
|
@@ -393,8 +393,9 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
393
393
|
* - `false` → invalid, no message
|
|
394
394
|
* - a non-empty `string` → invalid, string is the tooltip
|
|
395
395
|
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
396
|
+
* On its own `validate` flags: the value keeps rendering as-is and an
|
|
397
|
+
* edit that fails it still lands. Set `rejectInvalid` to refuse such an
|
|
398
|
+
* edit instead.
|
|
398
399
|
*/
|
|
399
400
|
validate?: (params: {
|
|
400
401
|
value: unknown
|
|
@@ -402,6 +403,16 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
402
403
|
rowIndex: number
|
|
403
404
|
column: Column<TData>
|
|
404
405
|
}) => string | boolean | null | undefined
|
|
406
|
+
/**
|
|
407
|
+
* Refuse an edit that `validate` flags. When true, a committed value that
|
|
408
|
+
* makes `validate` return `false` or a message is not written: the cell
|
|
409
|
+
* keeps its old value, no history step is recorded and
|
|
410
|
+
* `onCellValueChange` does not fire. The value is checked after
|
|
411
|
+
* `valueParser` has run, so the rule sees what would be stored. Applies to
|
|
412
|
+
* the inline editor, a commit to a selection and a full-row edit; paste
|
|
413
|
+
* does not go through the editor and is not checked.
|
|
414
|
+
*/
|
|
415
|
+
rejectInvalid?: boolean
|
|
405
416
|
/**
|
|
406
417
|
* Gate editing per column or per cell.
|
|
407
418
|
*
|
|
@@ -786,12 +797,29 @@ type BaseRowCtx<TData extends RowData> = {
|
|
|
786
797
|
const ROW_CTX = Symbol('svgrid.row.ctx')
|
|
787
798
|
const ROW_VALUES = Symbol('svgrid.row.values')
|
|
788
799
|
const ROW_CELLS = Symbol('svgrid.row.cells')
|
|
800
|
+
// Where the filtered stage last put this row: the generation of that full
|
|
801
|
+
// filter and the row's index in its output. Lets a tick swap a replaced row
|
|
802
|
+
// into place without re-filtering everything (see createFilteredRowModel).
|
|
803
|
+
const ROW_FILTER_GEN = Symbol('svgrid.row.filterGen')
|
|
804
|
+
const ROW_FILTER_POS = Symbol('svgrid.row.filterPos')
|
|
805
|
+
// Stamped on the rows a sort repair drops, so the merge skips them with one
|
|
806
|
+
// property read per row instead of a Set lookup (3 ms per 100k rows).
|
|
807
|
+
const ROW_SORT_DROPPED = Symbol('svgrid.row.sortDropped')
|
|
808
|
+
// The key(s) the last full sort computed for a row: the key itself for a
|
|
809
|
+
// one-clause sort, an array for several. Read by the repair path so a tick
|
|
810
|
+
// never recomputes a kept row's key.
|
|
811
|
+
const ROW_SORT_KEY = Symbol('svgrid.row.sortKey')
|
|
812
|
+
let sortDropGeneration = 0
|
|
789
813
|
|
|
790
814
|
/** A base row's private fields, on top of the public {@link Row} surface. */
|
|
791
815
|
type BaseRowState<TData extends RowData> = Row<TData> & {
|
|
792
816
|
[ROW_CTX]: BaseRowCtx<TData>
|
|
793
817
|
[ROW_VALUES]: Array<unknown> | null
|
|
794
818
|
[ROW_CELLS]: Array<Cell<TData>> | null
|
|
819
|
+
[ROW_FILTER_GEN]: number
|
|
820
|
+
[ROW_FILTER_POS]: number
|
|
821
|
+
[ROW_SORT_DROPPED]: number
|
|
822
|
+
[ROW_SORT_KEY]: unknown
|
|
795
823
|
}
|
|
796
824
|
|
|
797
825
|
/**
|
|
@@ -912,9 +940,26 @@ export function createCoreRowModel<TData extends RowData>(): RowModelFactory<TDa
|
|
|
912
940
|
* `columnFilteringFeature`; without it there are no filters to apply.
|
|
913
941
|
*/
|
|
914
942
|
export function createFilteredRowModel<TData extends RowData>(): RowModelFactory<TData> {
|
|
943
|
+
/**
|
|
944
|
+
* The last full filter, so a tick can repair it. When `getRowModel`
|
|
945
|
+
* reports which rows were replaced (`replacedRowsOf`) and the filter state
|
|
946
|
+
* and columns are the same objects as last time, each replacement is tested
|
|
947
|
+
* on its own: a row that stays in keeps its slot, a row that stays out
|
|
948
|
+
* stays out, and a row whose membership flips sends the whole thing back
|
|
949
|
+
* through the full pass, because an insertion has to land in data order.
|
|
950
|
+
* Positions come from stamps on the rows (ROW_FILTER_GEN / ROW_FILTER_POS)
|
|
951
|
+
* written by the full pass; no map is built.
|
|
952
|
+
*/
|
|
953
|
+
let cache: { input: Array<Row<TData>>; output: Array<Row<TData>>; filters: ColumnFiltersState; columns: Array<Column<TData>>; gen: number } | null = null
|
|
954
|
+
let generation = 0
|
|
955
|
+
|
|
915
956
|
return ({ table, rows }) => {
|
|
916
957
|
const filters: ColumnFiltersState = table.getState().columnFilters ?? []
|
|
917
|
-
if (!filters.length)
|
|
958
|
+
if (!filters.length) {
|
|
959
|
+
cache = null
|
|
960
|
+
return rows
|
|
961
|
+
}
|
|
962
|
+
const columns = table.getAllColumns()
|
|
918
963
|
|
|
919
964
|
// Resolve each filter's match function once, outside the row loop.
|
|
920
965
|
const compiled = filters.map((filter) => ({
|
|
@@ -922,8 +967,7 @@ export function createFilteredRowModel<TData extends RowData>(): RowModelFactory
|
|
|
922
967
|
value: filter.value,
|
|
923
968
|
fn: filter.fn ? filterFns[filter.fn] : filterFns.includesString,
|
|
924
969
|
}))
|
|
925
|
-
|
|
926
|
-
return rows.filter((row) => {
|
|
970
|
+
const passes = (row: Row<TData>) => {
|
|
927
971
|
for (let i = 0; i < compiled.length; i++) {
|
|
928
972
|
const filter = compiled[i]!
|
|
929
973
|
// `getCellValueByColumnId` rather than `getAllCells().find(...)`.
|
|
@@ -935,7 +979,48 @@ export function createFilteredRowModel<TData extends RowData>(): RowModelFactory
|
|
|
935
979
|
if (!filter.fn(row.getCellValueByColumnId(filter.id), filter.value as any)) return false
|
|
936
980
|
}
|
|
937
981
|
return true
|
|
938
|
-
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (cache && cache.filters === filters && cache.columns === columns) {
|
|
985
|
+
if (cache.input === rows) return cache.output
|
|
986
|
+
const replaced = replacedRowsOf(table)
|
|
987
|
+
if (replaced && rows.length === cache.input.length) {
|
|
988
|
+
let out: Array<Row<TData>> | null = null
|
|
989
|
+
let ok = true
|
|
990
|
+
for (let i = 0; i < rows.length && ok; i++) {
|
|
991
|
+
const prev = cache.input[i]! as BaseRowState<TData>
|
|
992
|
+
const next = rows[i]! as BaseRowState<TData>
|
|
993
|
+
if (next === prev) continue
|
|
994
|
+
if (replaced.get(prev) !== next) { ok = false; break }
|
|
995
|
+
const wasIn = prev[ROW_FILTER_GEN] === cache.gen
|
|
996
|
+
const isIn = passes(next)
|
|
997
|
+
if (wasIn !== isIn) { ok = false; break }
|
|
998
|
+
if (!isIn) continue
|
|
999
|
+
out ??= cache.output.slice()
|
|
1000
|
+
const pos = prev[ROW_FILTER_POS]
|
|
1001
|
+
out[pos] = next
|
|
1002
|
+
next[ROW_FILTER_GEN] = cache.gen
|
|
1003
|
+
next[ROW_FILTER_POS] = pos
|
|
1004
|
+
}
|
|
1005
|
+
if (ok) {
|
|
1006
|
+
const output = out ?? cache.output
|
|
1007
|
+
cache = { ...cache, input: rows, output }
|
|
1008
|
+
return output
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const gen = ++generation
|
|
1014
|
+
const output: Array<Row<TData>> = []
|
|
1015
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1016
|
+
const row = rows[i]! as BaseRowState<TData>
|
|
1017
|
+
if (!passes(row)) continue
|
|
1018
|
+
row[ROW_FILTER_GEN] = gen
|
|
1019
|
+
row[ROW_FILTER_POS] = output.length
|
|
1020
|
+
output.push(row)
|
|
1021
|
+
}
|
|
1022
|
+
cache = { input: rows, output, filters, columns, gen }
|
|
1023
|
+
return output
|
|
939
1024
|
}
|
|
940
1025
|
}
|
|
941
1026
|
/**
|
|
@@ -1225,6 +1310,15 @@ export function flattenTreeData<T extends RowData>(
|
|
|
1225
1310
|
export function createExpandedRowModel<TData extends RowData>(): RowModelFactory<TData> {
|
|
1226
1311
|
return ({ table, rows }) => {
|
|
1227
1312
|
const expanded: ExpandedState = table.getState().expanded ?? {}
|
|
1313
|
+
// Nothing expanded means every collapsed row hides its children and the
|
|
1314
|
+
// output is the input, element for element. Returning it as is keeps the
|
|
1315
|
+
// array identity the stages downstream and the render window key on, and
|
|
1316
|
+
// skips a 100k-element copy per data change on a flat grid.
|
|
1317
|
+
let anyExpanded = false
|
|
1318
|
+
for (const id in expanded) {
|
|
1319
|
+
if (expanded[id]) { anyExpanded = true; break }
|
|
1320
|
+
}
|
|
1321
|
+
if (!anyExpanded) return rows
|
|
1228
1322
|
const flattened: Array<Row<TData>> = []
|
|
1229
1323
|
const visit = (row: Row<TData>) => {
|
|
1230
1324
|
flattened.push(row)
|
|
@@ -1241,13 +1335,76 @@ export function createExpandedRowModel<TData extends RowData>(): RowModelFactory
|
|
|
1241
1335
|
* override the built-in {@link sortFns} - useful for locale-aware or
|
|
1242
1336
|
* domain-specific ordering.
|
|
1243
1337
|
*/
|
|
1338
|
+
/**
|
|
1339
|
+
* Which rows the last `getRowModel` rebuild replaced, old row -> new row, for
|
|
1340
|
+
* the duration of the pipeline run that follows it.
|
|
1341
|
+
*
|
|
1342
|
+
* A live feed replaces a few hundred row objects in a hundred-thousand-row
|
|
1343
|
+
* array and hands the grid the new array. The base rows are rebuilt reusing
|
|
1344
|
+
* every row whose data object is the same (see `getRowModel`), so the rebuild
|
|
1345
|
+
* knows exactly which rows are new. The filtered and sorted stages read that
|
|
1346
|
+
* here and repair their previous output instead of recomputing it, when the
|
|
1347
|
+
* replacements are few and nothing else changed. Cleared after the run; a
|
|
1348
|
+
* stage that reads it outside a run sees null and does the full work.
|
|
1349
|
+
*
|
|
1350
|
+
* Keyed on the grid object in a WeakMap rather than carried on the public
|
|
1351
|
+
* `SvGrid` type, so the stage signature stays `({ table, rows }) => rows`.
|
|
1352
|
+
*/
|
|
1353
|
+
const replacedRowsByGrid = new WeakMap<object, ReadonlyMap<Row<any>, Row<any>> | null>()
|
|
1354
|
+
|
|
1355
|
+
/** The rows `getRowModel` replaced in the rebuild this pipeline run follows, or null. */
|
|
1356
|
+
export function replacedRowsOf(table: SvGrid<any>): ReadonlyMap<Row<any>, Row<any>> | null {
|
|
1357
|
+
return replacedRowsByGrid.get(table) ?? null
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* The sorting stage of the row pipeline: orders the rows by the grid's
|
|
1362
|
+
* `sorting` state with the built-in comparators (or the `localSortFns` map
|
|
1363
|
+
* handed in), stable, equal keys keeping their data order. A tick that
|
|
1364
|
+
* replaces a few rows out of many is repaired incrementally rather than
|
|
1365
|
+
* re-sorted (see the note inside). Registered by `rowSortingFeature`; an app
|
|
1366
|
+
* only calls this to install its own comparator set.
|
|
1367
|
+
*/
|
|
1244
1368
|
export function createSortedRowModel<TData extends RowData>(
|
|
1245
1369
|
localSortFns: typeof sortFns = sortFns,
|
|
1246
1370
|
): RowModelFactory<TData> {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1371
|
+
/**
|
|
1372
|
+
* What the last full sort produced, kept so a tick can repair it.
|
|
1373
|
+
*
|
|
1374
|
+
* A live feed replaces a few hundred row objects out of a hundred thousand
|
|
1375
|
+
* and hands the grid a new array. Re-sorting all of it costs the same as
|
|
1376
|
+
* the first sort did (115-149 ms in the browser for 100k rows, see
|
|
1377
|
+
* docs/help/benchmarks.md); the order it would produce differs from the
|
|
1378
|
+
* previous one only where the replaced rows land. So the previous output
|
|
1379
|
+
* is kept, with its sort keys aligned to it, and when `getRowModel` says
|
|
1380
|
+
* which rows were replaced (see `replacedRowsOf`) the stage drops those,
|
|
1381
|
+
* sorts the replacements among themselves and merges them back in one
|
|
1382
|
+
* linear pass: O(n + k log k) instead of O(n log n) key builds and
|
|
1383
|
+
* comparisons.
|
|
1384
|
+
*
|
|
1385
|
+
* The repair is only attempted when everything else is the same object as
|
|
1386
|
+
* last time - the sorting state, the columns, and every input row except
|
|
1387
|
+
* the replaced ones - and it produces exactly what the full sort would:
|
|
1388
|
+
* equal keys fall back to the data index, which is the order a stable sort
|
|
1389
|
+
* over data-ordered input keeps.
|
|
1390
|
+
*/
|
|
1391
|
+
let cache: {
|
|
1392
|
+
input: Array<Row<TData>>
|
|
1393
|
+
output: Array<Row<TData>>
|
|
1394
|
+
sorting: SortingState
|
|
1395
|
+
columns: Array<Column<TData>>
|
|
1396
|
+
clauses: Array<SortClause>
|
|
1397
|
+
} | null = null
|
|
1398
|
+
|
|
1399
|
+
type SortClause = {
|
|
1400
|
+
desc: boolean
|
|
1401
|
+
compare: (a: any, b: any) => number
|
|
1402
|
+
/** The key for one row, or `MISSING_KEY` when it cannot be produced incrementally. */
|
|
1403
|
+
keyOf: (row: Row<TData>) => any
|
|
1404
|
+
}
|
|
1405
|
+
const MISSING_KEY = Symbol('svgrid.sort.missing-key')
|
|
1250
1406
|
|
|
1407
|
+
function fullSort(rows: Array<Row<TData>>, sorting: SortingState, allColumns: Array<Column<TData>>) {
|
|
1251
1408
|
// Resolve every clause ONCE, before sorting.
|
|
1252
1409
|
//
|
|
1253
1410
|
// This used to live inside the comparator, so `getAllColumns().find(...)`
|
|
@@ -1255,11 +1412,11 @@ export function createSortedRowModel<TData extends RowData>(
|
|
|
1255
1412
|
// 1,528,947 array scans, and a three-clause sort made 3,933,751 (measured;
|
|
1256
1413
|
// `pnpm bench --case=sort-1col`). The comparator is called O(n log n)
|
|
1257
1414
|
// times, so anything inside it that is not O(1) sets the cost of the sort.
|
|
1258
|
-
const allColumns = table.getAllColumns()
|
|
1259
1415
|
const clauses: Array<{
|
|
1260
1416
|
keys: Array<any>
|
|
1261
1417
|
desc: boolean
|
|
1262
1418
|
compare: (a: any, b: any) => number
|
|
1419
|
+
keyOf: (row: Row<TData>) => any
|
|
1263
1420
|
}> = []
|
|
1264
1421
|
|
|
1265
1422
|
for (const clause of sorting) {
|
|
@@ -1288,14 +1445,15 @@ export function createSortedRowModel<TData extends RowData>(
|
|
|
1288
1445
|
const n = rows.length
|
|
1289
1446
|
let keys: Array<any> = new Array(n)
|
|
1290
1447
|
let compare: (a: any, b: any) => number
|
|
1448
|
+
let keyOf: (row: Row<TData>) => any
|
|
1291
1449
|
|
|
1292
1450
|
if (comparator === sortFns.number) {
|
|
1293
|
-
|
|
1451
|
+
keyOf = (row) => Number(row.getCellValueByColumnId(columnId) ?? 0)
|
|
1452
|
+
for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)
|
|
1294
1453
|
compare = compareNumericKeys
|
|
1295
1454
|
} else if (comparator === sortFns.date) {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
}
|
|
1455
|
+
keyOf = (row) => new Date(row.getCellValueByColumnId(columnId) as any).getTime()
|
|
1456
|
+
for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)
|
|
1299
1457
|
compare = compareNumericKeys
|
|
1300
1458
|
} else if (comparator === sortFns.auto) {
|
|
1301
1459
|
const strings: string[] = new Array(n)
|
|
@@ -1354,19 +1512,24 @@ export function createSortedRowModel<TData extends RowData>(
|
|
|
1354
1512
|
for (let i = 0; i < ordered.length; i++) rankOf.set(ordered[i]!, i)
|
|
1355
1513
|
for (let i = 0; i < n; i++) keys[i] = rankOf.get(strings[i]!)!
|
|
1356
1514
|
compare = compareNumericKeys
|
|
1515
|
+
// A replacement whose text is not among the ranked values has no
|
|
1516
|
+
// rank; the repair path treats that as "sort everything again".
|
|
1517
|
+
keyOf = (row) => rankOf.get(String(row.getCellValueByColumnId(columnId))) ?? MISSING_KEY
|
|
1357
1518
|
} else {
|
|
1358
1519
|
keys = strings
|
|
1359
1520
|
compare = compareCollatedKeys
|
|
1521
|
+
keyOf = (row) => String(row.getCellValueByColumnId(columnId))
|
|
1360
1522
|
}
|
|
1361
1523
|
} else {
|
|
1362
|
-
|
|
1524
|
+
keyOf = (row) => row.getCellValueByColumnId(columnId)
|
|
1525
|
+
for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)
|
|
1363
1526
|
compare = comparator
|
|
1364
1527
|
}
|
|
1365
1528
|
|
|
1366
|
-
clauses.push({ keys, desc: clause.desc, compare })
|
|
1529
|
+
clauses.push({ keys, desc: clause.desc, compare, keyOf })
|
|
1367
1530
|
}
|
|
1368
1531
|
|
|
1369
|
-
if (!clauses.length) return
|
|
1532
|
+
if (!clauses.length) return null
|
|
1370
1533
|
|
|
1371
1534
|
// Sort an index array, then materialise. `Array.prototype.sort` is stable,
|
|
1372
1535
|
// so equal keys keep their original relative order exactly as the previous
|
|
@@ -1415,7 +1578,159 @@ export function createSortedRowModel<TData extends RowData>(
|
|
|
1415
1578
|
|
|
1416
1579
|
const sorted = new Array<Row<TData>>(rows.length)
|
|
1417
1580
|
for (let i = 0; i < order.length; i++) sorted[i] = rows[order[i]!]!
|
|
1418
|
-
|
|
1581
|
+
// Each row keeps its key(s), so a later tick can place a replacement
|
|
1582
|
+
// among the kept rows without reading any of them again. One property
|
|
1583
|
+
// write per row; an array per row only when there are several clauses.
|
|
1584
|
+
if (clauses.length === 1) {
|
|
1585
|
+
const keys = clauses[0]!.keys
|
|
1586
|
+
for (let i = 0; i < rows.length; i++) (rows[i] as any)[ROW_SORT_KEY] = keys[i]
|
|
1587
|
+
} else {
|
|
1588
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1589
|
+
const per = new Array(clauses.length)
|
|
1590
|
+
for (let k = 0; k < clauses.length; k++) per[k] = clauses[k]!.keys[i]
|
|
1591
|
+
;(rows[i] as any)[ROW_SORT_KEY] = per
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
return { sorted, clauses: clauses.map(({ desc, compare, keyOf }) => ({ desc, compare, keyOf })) }
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* The previous output with the replaced rows swapped for their
|
|
1599
|
+
* replacements, in sorted position. Null when the input is not the cached
|
|
1600
|
+
* input plus replacements, or a replacement has no incremental key.
|
|
1601
|
+
*
|
|
1602
|
+
* Every row carries the key(s) the last full sort computed for it
|
|
1603
|
+
* (ROW_SORT_KEY: the key itself for one clause, an array for several), so
|
|
1604
|
+
* the merge moves row references and nothing else. At 100k rows the loop
|
|
1605
|
+
* that also copied an aligned key array cost 3.7 ms; this one is the
|
|
1606
|
+
* reference copy alone.
|
|
1607
|
+
*/
|
|
1608
|
+
function repair(rows: Array<Row<TData>>, replaced: ReadonlyMap<Row<any>, Row<any>>) {
|
|
1609
|
+
const c = cache!
|
|
1610
|
+
if (rows.length !== c.input.length) return null
|
|
1611
|
+
// Which rows of THIS stage's input changed. The map may name rows a
|
|
1612
|
+
// stage upstream filtered out; only the ones that reach here matter.
|
|
1613
|
+
const dropGen = ++sortDropGeneration
|
|
1614
|
+
const added: Array<Row<TData>> = []
|
|
1615
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1616
|
+
const prev = c.input[i]!
|
|
1617
|
+
const next = rows[i]!
|
|
1618
|
+
if (next === prev) continue
|
|
1619
|
+
if (replaced.get(prev) !== next) return null
|
|
1620
|
+
;(prev as any)[ROW_SORT_DROPPED] = dropGen
|
|
1621
|
+
added.push(next)
|
|
1622
|
+
}
|
|
1623
|
+
if (!added.length) return c.output
|
|
1624
|
+
|
|
1625
|
+
const clauses = c.clauses
|
|
1626
|
+
const single = clauses.length === 1
|
|
1627
|
+
for (let j = 0; j < added.length; j++) {
|
|
1628
|
+
const row = added[j]! as any
|
|
1629
|
+
if (single) {
|
|
1630
|
+
const key = clauses[0]!.keyOf(row)
|
|
1631
|
+
if (key === MISSING_KEY) return null
|
|
1632
|
+
row[ROW_SORT_KEY] = key
|
|
1633
|
+
} else {
|
|
1634
|
+
const keys = new Array(clauses.length)
|
|
1635
|
+
for (let k = 0; k < clauses.length; k++) {
|
|
1636
|
+
const key = clauses[k]!.keyOf(row)
|
|
1637
|
+
if (key === MISSING_KEY) return null
|
|
1638
|
+
keys[k] = key
|
|
1639
|
+
}
|
|
1640
|
+
row[ROW_SORT_KEY] = keys
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// Keys first, then the data index: the tie-break a stable sort over
|
|
1645
|
+
// data-ordered input applies, so the merge lands where a full sort would.
|
|
1646
|
+
const prevOut = c.output
|
|
1647
|
+
const compareRows = single
|
|
1648
|
+
? (() => {
|
|
1649
|
+
const { compare, desc } = clauses[0]!
|
|
1650
|
+
return (a: Row<TData>, b: Row<TData>) => {
|
|
1651
|
+
const r = compare((a as any)[ROW_SORT_KEY], (b as any)[ROW_SORT_KEY])
|
|
1652
|
+
if (r !== 0) return desc ? -r : r
|
|
1653
|
+
return a.index - b.index
|
|
1654
|
+
}
|
|
1655
|
+
})()
|
|
1656
|
+
: (a: Row<TData>, b: Row<TData>) => {
|
|
1657
|
+
const ka = (a as any)[ROW_SORT_KEY]
|
|
1658
|
+
const kb = (b as any)[ROW_SORT_KEY]
|
|
1659
|
+
for (let k = 0; k < clauses.length; k++) {
|
|
1660
|
+
const clause = clauses[k]!
|
|
1661
|
+
const r = clause.compare(ka[k], kb[k])
|
|
1662
|
+
if (r !== 0) return clause.desc ? -r : r
|
|
1663
|
+
}
|
|
1664
|
+
return a.index - b.index
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// Sort the replacements among themselves (k log k)...
|
|
1668
|
+
added.sort(compareRows)
|
|
1669
|
+
|
|
1670
|
+
// ...find where each one goes in the previous output by binary search
|
|
1671
|
+
// (k log n comparisons, against the dropped rows too - they are skipped
|
|
1672
|
+
// below, which does not move an insertion point)...
|
|
1673
|
+
const n = prevOut.length
|
|
1674
|
+
const insertAt = new Array<number>(added.length)
|
|
1675
|
+
let lo = 0
|
|
1676
|
+
for (let j = 0; j < added.length; j++) {
|
|
1677
|
+
// Monotone: the replacements are sorted, so each search starts where
|
|
1678
|
+
// the previous one landed.
|
|
1679
|
+
const row = added[j]!
|
|
1680
|
+
let hi = n
|
|
1681
|
+
while (lo < hi) {
|
|
1682
|
+
const mid = (lo + hi) >>> 1
|
|
1683
|
+
if (compareRows(row, prevOut[mid]!) < 0) hi = mid
|
|
1684
|
+
else lo = mid + 1
|
|
1685
|
+
}
|
|
1686
|
+
insertAt[j] = lo
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// ...then one linear pass that only moves references.
|
|
1690
|
+
const out = new Array<Row<TData>>(rows.length)
|
|
1691
|
+
let w = 0
|
|
1692
|
+
let j = 0
|
|
1693
|
+
let nextInsert = insertAt[0] ?? -1
|
|
1694
|
+
for (let i = 0; i < n; i++) {
|
|
1695
|
+
while (nextInsert === i) {
|
|
1696
|
+
out[w++] = added[j++]!
|
|
1697
|
+
nextInsert = j < added.length ? insertAt[j]! : -1
|
|
1698
|
+
}
|
|
1699
|
+
const prev = prevOut[i]!
|
|
1700
|
+
if ((prev as any)[ROW_SORT_DROPPED] === dropGen) continue
|
|
1701
|
+
out[w++] = prev
|
|
1702
|
+
}
|
|
1703
|
+
while (j < added.length) out[w++] = added[j++]!
|
|
1704
|
+
return out
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
return function sortedRowModelStage({ table, rows }) {
|
|
1708
|
+
const sorting = table.getState().sorting ?? []
|
|
1709
|
+
if (!sorting.length) {
|
|
1710
|
+
cache = null
|
|
1711
|
+
return rows
|
|
1712
|
+
}
|
|
1713
|
+
const columns = table.getAllColumns()
|
|
1714
|
+
|
|
1715
|
+
if (cache && cache.sorting === sorting && cache.columns === columns) {
|
|
1716
|
+
if (cache.input === rows) return cache.output
|
|
1717
|
+
const replaced = replacedRowsOf(table)
|
|
1718
|
+
if (replaced) {
|
|
1719
|
+
const repaired = repair(rows, replaced)
|
|
1720
|
+
if (repaired) {
|
|
1721
|
+
cache = { ...cache, input: rows, output: repaired }
|
|
1722
|
+
return repaired
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
const full = fullSort(rows, sorting, columns)
|
|
1728
|
+
if (!full) {
|
|
1729
|
+
cache = null
|
|
1730
|
+
return rows
|
|
1731
|
+
}
|
|
1732
|
+
cache = { input: rows, output: full.sorted, sorting, columns, clauses: full.clauses }
|
|
1733
|
+
return full.sorted
|
|
1419
1734
|
}
|
|
1420
1735
|
}
|
|
1421
1736
|
|
|
@@ -1545,6 +1860,8 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1545
1860
|
let cachedBaseRowsInput: ReadonlyArray<TData> | null = null
|
|
1546
1861
|
let cachedBaseRowsColumns: Array<Column<TData>> | null = null
|
|
1547
1862
|
let cachedBaseRows: Array<Row<TData>> = []
|
|
1863
|
+
/** Old row -> new row for the rebuild the next pipeline run follows; see `replacedRowsOf`. */
|
|
1864
|
+
let replacedRows: Map<Row<TData>, Row<TData>> | null = null
|
|
1548
1865
|
let cachedRowCtx: BaseRowCtx<TData> | null = null
|
|
1549
1866
|
let cachedRowModel: RowModel<TData> | null = null
|
|
1550
1867
|
let cachedRowModelBaseRows: Array<Row<TData>> | null = null
|
|
@@ -1754,7 +2071,11 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1754
2071
|
},
|
|
1755
2072
|
getRowModel() {
|
|
1756
2073
|
const columns = grid.getAllColumns()
|
|
1757
|
-
|
|
2074
|
+
// Read once. `options.data` is a getter when <SvGrid> drives the engine
|
|
2075
|
+
// (a $state.raw signal), and the rebuild below touched it twice per row:
|
|
2076
|
+
// 200k tracked reads per 100k-row tick, 11 ms of a 65 ms tick.
|
|
2077
|
+
const data = options.data
|
|
2078
|
+
if (cachedBaseRowsInput !== data || cachedBaseRowsColumns !== columns) {
|
|
1758
2079
|
// Same columns as last time: the shared context still describes them,
|
|
1759
2080
|
// and a row whose data object sits at the same index can keep its
|
|
1760
2081
|
// row object. A row model that streams blocks hands the grid a new
|
|
@@ -1764,7 +2085,11 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1764
2085
|
// an app may have changed the object in place before passing a new
|
|
1765
2086
|
// array - that is the case the old rebuild covered by accident.
|
|
1766
2087
|
const previous = cachedBaseRowsColumns === columns && cachedRowCtx ? cachedBaseRows : null
|
|
1767
|
-
|
|
2088
|
+
// Same length as last time: a replacement, not an add or remove, so
|
|
2089
|
+
// each new row stands in for the old row at its index and the stages
|
|
2090
|
+
// can repair rather than recompute. Anything else is structural.
|
|
2091
|
+
replacedRows = previous && previous.length === data.length ? new Map() : null
|
|
2092
|
+
cachedBaseRowsInput = data
|
|
1768
2093
|
cachedBaseRowsColumns = columns
|
|
1769
2094
|
// O(1) column-id → index lookup so getCellValueByColumnId doesn't do
|
|
1770
2095
|
// a linear `findIndex` on every cell read (was O(rows × cells × cols)).
|
|
@@ -1786,7 +2111,7 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1786
2111
|
cachedRowCtx = rowCtx
|
|
1787
2112
|
}
|
|
1788
2113
|
|
|
1789
|
-
cachedBaseRows = new Array(
|
|
2114
|
+
cachedBaseRows = new Array(data.length)
|
|
1790
2115
|
const getRowId = options.getRowId
|
|
1791
2116
|
const m = BASE_ROW_METHODS as unknown as {
|
|
1792
2117
|
getCanExpand: Row<TData>['getCanExpand']
|
|
@@ -1797,8 +2122,8 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1797
2122
|
getAllCells: Row<TData>['getAllCells']
|
|
1798
2123
|
getCellValueByColumnId: Row<TData>['getCellValueByColumnId']
|
|
1799
2124
|
}
|
|
1800
|
-
for (let index = 0; index <
|
|
1801
|
-
const original =
|
|
2125
|
+
for (let index = 0; index < data.length; index++) {
|
|
2126
|
+
const original = data[index]!
|
|
1802
2127
|
const kept = previous?.[index] as BaseRowState<TData> | undefined
|
|
1803
2128
|
if (kept && kept.original === original) {
|
|
1804
2129
|
kept[ROW_VALUES] = null
|
|
@@ -1806,6 +2131,9 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1806
2131
|
cachedBaseRows[index] = kept
|
|
1807
2132
|
continue
|
|
1808
2133
|
}
|
|
2134
|
+
// Past a quarter of the rows the merge stops being cheaper than a
|
|
2135
|
+
// sort with the ranking pass; let the stages do the full work.
|
|
2136
|
+
if (replacedRows && replacedRows.size * 4 >= data.length) replacedRows = null
|
|
1809
2137
|
// `_values` and `_cells` stay null until something reads them - a
|
|
1810
2138
|
// 100k-row grid showing twenty rows must not materialise every row's
|
|
1811
2139
|
// values or cell objects to paint.
|
|
@@ -1817,6 +2145,10 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1817
2145
|
[ROW_CTX]: rowCtx,
|
|
1818
2146
|
[ROW_VALUES]: null,
|
|
1819
2147
|
[ROW_CELLS]: null,
|
|
2148
|
+
[ROW_FILTER_GEN]: 0,
|
|
2149
|
+
[ROW_FILTER_POS]: -1,
|
|
2150
|
+
[ROW_SORT_DROPPED]: 0,
|
|
2151
|
+
[ROW_SORT_KEY]: undefined,
|
|
1820
2152
|
getCanExpand: m.getCanExpand,
|
|
1821
2153
|
getIsExpanded: m.getIsExpanded,
|
|
1822
2154
|
toggleExpanded: m.toggleExpanded,
|
|
@@ -1826,7 +2158,15 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1826
2158
|
getCellValueByColumnId: m.getCellValueByColumnId,
|
|
1827
2159
|
}
|
|
1828
2160
|
cachedBaseRows[index] = row
|
|
2161
|
+
if (replacedRows && kept) replacedRows.set(kept, row)
|
|
1829
2162
|
}
|
|
2163
|
+
// A new array with every object the same is the documented way to
|
|
2164
|
+
// say "I changed rows in place, re-read them" (docs/help/cells/
|
|
2165
|
+
// view-refresh.md). Nothing was replaced, so there is nothing to
|
|
2166
|
+
// repair around: the stages recompute from the current values. A
|
|
2167
|
+
// repair assumes the rows it keeps are unchanged in value as well as
|
|
2168
|
+
// identity, which is the immutable-update contract a tick follows.
|
|
2169
|
+
if (replacedRows && replacedRows.size === 0) replacedRows = null
|
|
1830
2170
|
}
|
|
1831
2171
|
|
|
1832
2172
|
// Only the slices a pipeline stage actually READS belong in this key.
|
|
@@ -1873,9 +2213,17 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1873
2213
|
pipeline.expandedRowModel,
|
|
1874
2214
|
pipeline.paginatedRowModel,
|
|
1875
2215
|
]
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2216
|
+
// The stages read the replacements through `replacedRowsOf` while the
|
|
2217
|
+
// run lasts; a stage that runs outside one sees null.
|
|
2218
|
+
replacedRowsByGrid.set(grid, replacedRows)
|
|
2219
|
+
try {
|
|
2220
|
+
ordered.forEach((fn) => {
|
|
2221
|
+
if (fn) rows = fn({ table: grid, rows })
|
|
2222
|
+
})
|
|
2223
|
+
} finally {
|
|
2224
|
+
replacedRowsByGrid.set(grid, null)
|
|
2225
|
+
replacedRows = null
|
|
2226
|
+
}
|
|
1879
2227
|
cachedPipeline = options._rowModels
|
|
1880
2228
|
cachedSlices = currentSlices
|
|
1881
2229
|
cachedRowModelBaseRows = cachedBaseRows
|
package/src/editing.test.ts
CHANGED
|
@@ -14,6 +14,8 @@ interface ColSpec {
|
|
|
14
14
|
editable?: boolean | ((cellCtx: any) => boolean)
|
|
15
15
|
editorType?: string
|
|
16
16
|
editorMultiple?: boolean
|
|
17
|
+
validate?: (p: { value: unknown; row: any }) => string | boolean | null | undefined
|
|
18
|
+
rejectInvalid?: boolean
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
interface FakeOptions {
|
|
@@ -40,6 +42,8 @@ function makeCtx(opts: FakeOptions = {}) {
|
|
|
40
42
|
editable: c.editable,
|
|
41
43
|
editorType: c.editorType,
|
|
42
44
|
editorMultiple: c.editorMultiple,
|
|
45
|
+
validate: c.validate,
|
|
46
|
+
rejectInvalid: c.rejectInvalid,
|
|
43
47
|
},
|
|
44
48
|
}))
|
|
45
49
|
|
|
@@ -381,6 +385,54 @@ describe('saveEditingCell', () => {
|
|
|
381
385
|
expect(ctx.internalData[0].a).toEqual(['x', 'y'])
|
|
382
386
|
})
|
|
383
387
|
|
|
388
|
+
it('refuses a value the column validates against when rejectInvalid is set', () => {
|
|
389
|
+
const onCellValueChange = vi.fn()
|
|
390
|
+
const { ctx, ed } = editingFor({
|
|
391
|
+
columns: [{
|
|
392
|
+
id: 'b', field: 'b', editorType: 'number', rejectInvalid: true,
|
|
393
|
+
validate: ({ value }) => (Number(value) < 0 ? 'Amount must be positive' : null),
|
|
394
|
+
}],
|
|
395
|
+
data: [{ b: 5 }],
|
|
396
|
+
})
|
|
397
|
+
ctx.props.onCellValueChange = onCellValueChange
|
|
398
|
+
ctx.editingCell = { rowId: '0', columnId: 'b', editorType: 'number', value: '-5' }
|
|
399
|
+
ed.saveEditingCell()
|
|
400
|
+
// The row, the overlay, the history and the consumer are untouched; the
|
|
401
|
+
// editor closes.
|
|
402
|
+
expect(ctx.internalData[0].b).toBe(5)
|
|
403
|
+
expect('0:b' in ctx.editedCellValues).toBe(false)
|
|
404
|
+
expect(ctx.history).toHaveLength(0)
|
|
405
|
+
expect(onCellValueChange).not.toHaveBeenCalled()
|
|
406
|
+
expect(ctx.editingCell).toBeNull()
|
|
407
|
+
|
|
408
|
+
// A value the rule accepts still lands.
|
|
409
|
+
ctx.editingCell = { rowId: '0', columnId: 'b', editorType: 'number', value: '7' }
|
|
410
|
+
ed.saveEditingCell()
|
|
411
|
+
expect(ctx.internalData[0].b).toBe(7)
|
|
412
|
+
expect(onCellValueChange).toHaveBeenCalledTimes(1)
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('without rejectInvalid a flagged value lands, as before', () => {
|
|
416
|
+
const { ctx, ed } = editingFor({
|
|
417
|
+
columns: [{ id: 'b', field: 'b', editorType: 'number', validate: () => 'always wrong' }],
|
|
418
|
+
data: [{ b: 5 }],
|
|
419
|
+
})
|
|
420
|
+
ctx.editingCell = { rowId: '0', columnId: 'b', editorType: 'number', value: '-5' }
|
|
421
|
+
ed.saveEditingCell()
|
|
422
|
+
expect(ctx.internalData[0].b).toBe(-5)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('checks the value after valueParser, so the rule sees what would be stored', () => {
|
|
426
|
+
const { ctx, ed } = editingFor({
|
|
427
|
+
columns: [{ id: 'b', field: 'b', editorType: 'number', rejectInvalid: true, validate: ({ value }) => (Number(value) > 100 ? false : null) }],
|
|
428
|
+
data: [{ b: 5 }],
|
|
429
|
+
})
|
|
430
|
+
;(ctx.allColumns[0].columnDef as any).valueParser = ({ newValue }: { newValue: number }) => newValue * 100
|
|
431
|
+
ctx.editingCell = { rowId: '0', columnId: 'b', editorType: 'number', value: '2' }
|
|
432
|
+
ed.saveEditingCell()
|
|
433
|
+
expect(ctx.internalData[0].b).toBe(5) // 200 after the parser, refused
|
|
434
|
+
})
|
|
435
|
+
|
|
384
436
|
it('pushes a history step and bumps the version on a real change', () => {
|
|
385
437
|
const { ctx, ed } = editingFor({
|
|
386
438
|
columns: [{ id: 'a', field: 'a', editorType: 'text' }],
|