@svgrid/grid 3.0.4 → 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 +32 -11
- package/dist/GridMenus.svelte +34 -4
- package/dist/SvGrid.controller.svelte.d.ts +33 -5
- package/dist/SvGrid.controller.svelte.js +397 -58
- package/dist/SvGrid.css +200 -2
- package/dist/SvGrid.svelte +350 -40
- package/dist/SvGrid.types.d.ts +433 -7
- 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 +92 -37
- package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-C-NtjlrJ.js} +155 -146
- 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-DIqb3VZn.js +241 -0
- package/dist/cdn/{src-V1uu8iE9.js → src-CgoeSM8k.js} +5339 -4753
- package/dist/cdn/{src-BKhZ6eXd.js → src-DfwxB1TH.js} +9817 -9231
- package/dist/cdn/svgrid.js +28 -27
- package/dist/cdn/svgrid.svelte-external.js +28 -27
- package/dist/cdn/validate-AZoD8BoR.js +80 -0
- 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 +401 -38
- package/dist/editing.js +34 -0
- package/dist/gantt-view.svelte.d.ts +24 -0
- package/dist/gantt-view.svelte.js +13 -0
- package/dist/grid-icons.d.ts +2 -2
- package/dist/grid-icons.js +2 -2
- package/dist/grid-messages.d.ts +10 -0
- package/dist/grid-messages.js +9 -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 +5 -4
- package/dist/index.js +3 -3
- 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 +139 -0
- package/dist/row-model.js +16 -0
- package/dist/row-resize.js +22 -1
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +31 -3
- package/dist/server-block-cache.d.ts +219 -0
- package/dist/server-block-cache.js +539 -0
- package/dist/server-data-source.d.ts +274 -8
- package/dist/server-data-source.js +229 -20
- package/dist/server.d.ts +13 -0
- package/dist/server.js +13 -0
- package/dist/sparkline.js +7 -5
- package/dist/svgrid-wrapper.types.d.ts +33 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +29 -3
- package/package.json +11 -1
- package/src/GridMenus.svelte +34 -4
- package/src/SvGrid.controller.svelte.ts +397 -61
- package/src/SvGrid.css +200 -2
- package/src/SvGrid.svelte +350 -40
- package/src/SvGrid.types.ts +453 -7
- 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 +96 -31
- 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.rowmodel-cache.test.ts +51 -0
- package/src/core.tick-repair.test.ts +287 -0
- package/src/core.ts +407 -38
- package/src/editing.test.ts +52 -0
- package/src/editing.ts +33 -0
- package/src/gantt-stub.test.svelte +38 -0
- package/src/gantt-view.svelte.ts +35 -0
- package/src/grid-icons.ts +2 -2
- package/src/grid-messages.test.ts +7 -0
- package/src/grid-messages.ts +22 -0
- package/src/headless.ts +24 -0
- package/src/history.test.ts +49 -1
- package/src/history.ts +48 -2
- package/src/icon-seam.test.ts +2 -4
- package/src/index.ts +38 -10
- 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 +152 -0
- package/src/row-resize.ts +24 -1
- package/src/selection.test.ts +3 -0
- package/src/selection.ts +33 -3
- package/src/server-block-cache.test.ts +672 -0
- package/src/server-block-cache.ts +690 -0
- package/src/server-data-source.infinite.test.ts +405 -0
- package/src/server-data-source.test.ts +19 -0
- package/src/server-data-source.ts +496 -29
- package/src/server.ts +49 -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 +199 -0
- 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-prop.svelte.test.ts +352 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +305 -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 +8 -5
- package/src/test-setup.ts +12 -0
- package/src/transaction.test.ts +58 -0
- package/src/validate.test.ts +29 -0
- package/src/validate.ts +34 -3
- package/dist/SvGroupCell.svelte +0 -141
- package/dist/SvGroupCell.svelte.d.ts +0 -49
- package/dist/SvRowGroupPanel.svelte +0 -186
- package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
- package/dist/cdn/GridMenus-7kbpnnBW.js +0 -635
- package/dist/cdn/validate-_CDJzgIo.js +0 -75
- package/dist/server-group-model.d.ts +0 -98
- package/dist/server-group-model.js +0 -263
- package/src/SvGroupCell.svelte +0 -141
- package/src/SvRowGroupPanel.svelte +0 -186
- package/src/server-group-model.test.ts +0 -294
- package/src/server-group-model.ts +0 -370
package/src/clipboard.ts
CHANGED
|
@@ -100,9 +100,9 @@ export function createClipboard<
|
|
|
100
100
|
if (before !== value) steps.push({ rowId: row.id, columnId, field, before, after: value });
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function applyFillPattern() {
|
|
103
|
+
function applyFillPattern(toggled = false) {
|
|
104
104
|
const steps: HistoryStep[] = [];
|
|
105
|
-
runHistoryGroup(ctx, () => applyFillCells(steps));
|
|
105
|
+
runHistoryGroup(ctx, () => applyFillCells(steps, toggled));
|
|
106
106
|
if (steps.length) pushHistory(ctx, steps, nextGroupId());
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -117,7 +117,7 @@ export function createClipboard<
|
|
|
117
117
|
pattern: unknown[],
|
|
118
118
|
i: number,
|
|
119
119
|
sourceStart: number,
|
|
120
|
-
|
|
120
|
+
at: number,
|
|
121
121
|
step: 1 | -1,
|
|
122
122
|
axis: "rows" | "cols",
|
|
123
123
|
rowIndex: number,
|
|
@@ -126,7 +126,10 @@ export function createClipboard<
|
|
|
126
126
|
const hook = ctx.props.processCellForFill;
|
|
127
127
|
if (hook && sources.length) {
|
|
128
128
|
const k = i % sources.length;
|
|
129
|
-
|
|
129
|
+
// The real distance to the cell being filled, which is not i when a
|
|
130
|
+
// row the fill skips lies between: a formula moves by the rows it
|
|
131
|
+
// actually travelled.
|
|
132
|
+
const distance = at - (sourceStart + step * k);
|
|
130
133
|
const delta = axis === "rows" ? { rows: distance, cols: 0 } : { rows: 0, cols: distance };
|
|
131
134
|
const decided = hook({ value: sources[k], delta, rowIndex, columnId });
|
|
132
135
|
if (decided !== undefined) return decided;
|
|
@@ -169,7 +172,18 @@ export function createClipboard<
|
|
|
169
172
|
applyFillPattern();
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
|
|
175
|
+
/** The fill's values the other way round from the pattern's default. */
|
|
176
|
+
function toggledPattern(sources: unknown[], count: number): unknown[] {
|
|
177
|
+
if (sources.length === 1 && typeof sources[0] === 'number' && Number.isFinite(sources[0])) {
|
|
178
|
+
return Array.from({ length: count }, (_, i) => (sources[0] as number) + i + 1);
|
|
179
|
+
}
|
|
180
|
+
return Array.from({ length: count }, (_, i) => sources[i % sources.length]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function applyFillCells(steps: HistoryStep[], toggled = false) {
|
|
184
|
+
const pattern = (sources: unknown[], count: number) => (toggled ? toggledPattern(sources, count) : buildFillPattern(sources, count));
|
|
185
|
+
const includeCollapsed = ctx.props.includeCollapsedRows as ((rowIndex: number) => boolean) | undefined;
|
|
186
|
+
const skipsRow = (r: number) => ctx.isRowCollapsed?.(r) === true && !(includeCollapsed?.(r) ?? false);
|
|
173
187
|
const d = ctx.fillDrag;
|
|
174
188
|
if (!d) return;
|
|
175
189
|
// Clear fillDrag FIRST so a thrown error doesn't leave the grid
|
|
@@ -197,27 +211,30 @@ export function createClipboard<
|
|
|
197
211
|
for (let r = d.sourceMinRow; r <= d.sourceMaxRow; r += 1) {
|
|
198
212
|
sourceColValues.push(readCellRaw(r, column.id));
|
|
199
213
|
}
|
|
214
|
+
// A collapsed row the consumer does not count (a filtered row in a
|
|
215
|
+
// spreadsheet) is neither written nor a step of the series: the
|
|
216
|
+
// pattern runs over the rows that are there, as Excel's does.
|
|
200
217
|
if (newMaxRow > d.sourceMaxRow) {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
218
|
+
const targets: number[] = [];
|
|
219
|
+
for (let r = d.sourceMaxRow + 1; r <= newMaxRow; r += 1) if (!skipsRow(r)) targets.push(r);
|
|
220
|
+
const fills = pattern(sourceColValues, targets.length);
|
|
221
|
+
targets.forEach((targetRow, i) => {
|
|
205
222
|
if (ctx.isCellEditableAt(targetRow, c))
|
|
206
223
|
writeFilled(steps, targetRow, column.id,
|
|
207
|
-
filledValue(sourceColValues, fills, i, d.sourceMinRow,
|
|
208
|
-
}
|
|
224
|
+
filledValue(sourceColValues, fills, i, d.sourceMinRow, targetRow, 1, "rows", targetRow, column.id));
|
|
225
|
+
});
|
|
209
226
|
}
|
|
210
227
|
if (newMinRow < d.sourceMinRow) {
|
|
211
228
|
// Filling upward - reverse-extrapolate.
|
|
212
229
|
const reversed = sourceColValues.slice().reverse();
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
230
|
+
const targets: number[] = [];
|
|
231
|
+
for (let r = d.sourceMinRow - 1; r >= newMinRow; r -= 1) if (!skipsRow(r)) targets.push(r);
|
|
232
|
+
const fills = pattern(reversed, targets.length);
|
|
233
|
+
targets.forEach((targetRow, i) => {
|
|
217
234
|
if (ctx.isCellEditableAt(targetRow, c))
|
|
218
235
|
writeFilled(steps, targetRow, column.id,
|
|
219
|
-
filledValue(reversed, fills, i, d.sourceMaxRow,
|
|
220
|
-
}
|
|
236
|
+
filledValue(reversed, fills, i, d.sourceMaxRow, targetRow, -1, "rows", targetRow, column.id));
|
|
237
|
+
});
|
|
221
238
|
}
|
|
222
239
|
}
|
|
223
240
|
} else if (horizontalExtension) {
|
|
@@ -232,25 +249,25 @@ export function createClipboard<
|
|
|
232
249
|
}
|
|
233
250
|
if (newMaxCol > d.sourceMaxCol) {
|
|
234
251
|
const targetCols = newMaxCol - d.sourceMaxCol;
|
|
235
|
-
const fills =
|
|
252
|
+
const fills = pattern(sourceRowValues, targetCols);
|
|
236
253
|
for (let i = 0; i < targetCols; i += 1) {
|
|
237
254
|
const targetCol = d.sourceMaxCol + 1 + i;
|
|
238
255
|
const col = ctx.allColumns[targetCol];
|
|
239
256
|
if (col && ctx.isCellEditableAt(r, targetCol))
|
|
240
257
|
writeFilled(steps, r, col.id,
|
|
241
|
-
filledValue(sourceRowValues, fills, i, d.sourceMinCol,
|
|
258
|
+
filledValue(sourceRowValues, fills, i, d.sourceMinCol, targetCol, 1, "cols", r, col.id));
|
|
242
259
|
}
|
|
243
260
|
}
|
|
244
261
|
if (newMinCol < d.sourceMinCol) {
|
|
245
262
|
const reversed = sourceRowValues.slice().reverse();
|
|
246
263
|
const targetCols = d.sourceMinCol - newMinCol;
|
|
247
|
-
const fills =
|
|
264
|
+
const fills = pattern(reversed, targetCols);
|
|
248
265
|
for (let i = 0; i < targetCols; i += 1) {
|
|
249
266
|
const targetCol = d.sourceMinCol - 1 - i;
|
|
250
267
|
const col = ctx.allColumns[targetCol];
|
|
251
268
|
if (col && ctx.isCellEditableAt(r, targetCol))
|
|
252
269
|
writeFilled(steps, r, col.id,
|
|
253
|
-
filledValue(reversed, fills, i, d.sourceMaxCol,
|
|
270
|
+
filledValue(reversed, fills, i, d.sourceMaxCol, targetCol, -1, "cols", r, col.id));
|
|
254
271
|
}
|
|
255
272
|
}
|
|
256
273
|
}
|
|
@@ -277,8 +294,10 @@ export function createClipboard<
|
|
|
277
294
|
const minCol = Math.min(anchor.colIndex, focus.colIndex);
|
|
278
295
|
const maxCol = Math.max(anchor.colIndex, focus.colIndex);
|
|
279
296
|
for (let r = minRow; r <= maxRow; r += 1) {
|
|
297
|
+
if (ctx.isRowCollapsed?.(r)) continue;
|
|
280
298
|
for (let c = minCol; c <= maxCol; c += 1) {
|
|
281
299
|
const col = ctx.allColumns[c];
|
|
300
|
+
if (col && ctx.collapsedColumns?.[col.id]) continue;
|
|
282
301
|
if (col?.columnDef.field && ctx.isCellEditableAt(r, c)) {
|
|
283
302
|
writeCellRaw(r, col.id, null);
|
|
284
303
|
}
|
|
@@ -448,9 +467,15 @@ export function createClipboard<
|
|
|
448
467
|
ctx.fillDrag = { ...ctx.fillDrag, targetRow: hit.row, targetCol: hit.col };
|
|
449
468
|
}
|
|
450
469
|
|
|
451
|
-
|
|
470
|
+
/**
|
|
471
|
+
* The release. Excel's Ctrl turns the fill the other way: a series
|
|
472
|
+
* becomes a copy of the block, and one number, which copies on its own,
|
|
473
|
+
* becomes a series stepping by one. The modifier is read here, at the
|
|
474
|
+
* release, so holding Ctrl only at the end still counts, as in Excel.
|
|
475
|
+
*/
|
|
476
|
+
function onFillPointerUp(event?: PointerEvent) {
|
|
452
477
|
if (!ctx.fillDrag) return;
|
|
453
|
-
applyFillPattern();
|
|
478
|
+
applyFillPattern(!!(event && (event.ctrlKey || event.metaKey)));
|
|
454
479
|
}
|
|
455
480
|
|
|
456
481
|
// ---- Range move / copy (drag the selection border) --------------------
|
|
@@ -837,6 +862,7 @@ export function createClipboard<
|
|
|
837
862
|
const processCell = ctx.props.processCellForClipboard as
|
|
838
863
|
| ((params: { value: unknown; column: unknown; row: unknown; rowIndex: number; columnId: string }) => unknown)
|
|
839
864
|
| undefined;
|
|
865
|
+
const copyCollapsed = ctx.props.includeCollapsedRows as ((rowIndex: number) => boolean) | undefined;
|
|
840
866
|
const blocks: Array<string> = [];
|
|
841
867
|
for (const rect of rects) {
|
|
842
868
|
const lines: Array<string> = [];
|
|
@@ -844,6 +870,7 @@ export function createClipboard<
|
|
|
844
870
|
const header: Array<string> = [];
|
|
845
871
|
for (let c = rect.minCol; c <= rect.maxCol; c += 1) {
|
|
846
872
|
const column = ctx.allColumns[c];
|
|
873
|
+
if (column && ctx.collapsedColumns?.[column.id]) continue;
|
|
847
874
|
header.push(column ? toolPanelHeaderLabel(column) : "");
|
|
848
875
|
}
|
|
849
876
|
lines.push(header.join("\t"));
|
|
@@ -851,6 +878,13 @@ export function createClipboard<
|
|
|
851
878
|
for (let r = rect.minRow; r <= rect.maxRow; r += 1) {
|
|
852
879
|
const row = ctx.allRows[r];
|
|
853
880
|
if (!row || isGroupRow(row)) continue;
|
|
881
|
+
// A collapsed row is folded to nothing: the user cannot see it and
|
|
882
|
+
// the keyboard walks past it, so a copy leaves it out too. That is
|
|
883
|
+
// what a spreadsheet does with a filtered row, and it is what makes
|
|
884
|
+
// "filter, copy, paste" carry the rows that matched. A consumer that
|
|
885
|
+
// tells hidden from filtered answers per row (`includeCollapsedRows`):
|
|
886
|
+
// Excel copies a row hidden by hand.
|
|
887
|
+
if (ctx.isRowCollapsed?.(r) && !copyCollapsed?.(r)) continue;
|
|
854
888
|
const cells: Array<string> = [];
|
|
855
889
|
for (let c = rect.minCol; c <= rect.maxCol; c += 1) {
|
|
856
890
|
const column = ctx.allColumns[c];
|
|
@@ -858,6 +892,7 @@ export function createClipboard<
|
|
|
858
892
|
cells.push("");
|
|
859
893
|
continue;
|
|
860
894
|
}
|
|
895
|
+
if (ctx.collapsedColumns?.[column.id]) continue;
|
|
861
896
|
const base = getColumnBaseValue(row, column);
|
|
862
897
|
let value: unknown = ctx.getCellDisplayValue(row.id, column.id, base);
|
|
863
898
|
if (processCell) {
|
|
@@ -916,6 +951,9 @@ export function createClipboard<
|
|
|
916
951
|
for (let r = startRow; r <= endRow; r += 1) {
|
|
917
952
|
const row = ctx.allRows[r];
|
|
918
953
|
if (!row || isGroupRow(row)) continue;
|
|
954
|
+
// A collapsed row is not there as far as the user is concerned, and a
|
|
955
|
+
// Delete over a filtered block must not wipe what the filter hid.
|
|
956
|
+
if (ctx.isRowCollapsed?.(r)) continue;
|
|
919
957
|
// By identity, not `Number(row.id)` - that is the array index only under
|
|
920
958
|
// the default getRowId, so a custom one cleared the wrong row.
|
|
921
959
|
const dataIndex = dataIndexOf(row);
|
|
@@ -929,6 +967,7 @@ export function createClipboard<
|
|
|
929
967
|
for (let c = startCol; c <= endCol; c += 1) {
|
|
930
968
|
const column = ctx.allColumns[c];
|
|
931
969
|
if (!column?.columnDef.field) continue;
|
|
970
|
+
if (ctx.collapsedColumns?.[column.id]) continue;
|
|
932
971
|
if (!ctx.isCellEditableAt(r, c)) continue;
|
|
933
972
|
// Clear means: empty string for text, undefined for everything
|
|
934
973
|
// else. parseEditorValue handles the per-type coercion.
|
package/src/columns.test.ts
CHANGED
|
@@ -57,14 +57,14 @@ describe('cellPinStyle', () => {
|
|
|
57
57
|
it('returns a left sticky style when the column has a left offset', () => {
|
|
58
58
|
const ctx = makeCtx({ pinnedOffsets: { left: { a: 0, b: 120 }, right: {} } })
|
|
59
59
|
const h = createColumns(ctx)
|
|
60
|
-
expect(h.cellPinStyle('a')).toBe('position: sticky;
|
|
61
|
-
expect(h.cellPinStyle('b')).toBe('position: sticky;
|
|
60
|
+
expect(h.cellPinStyle('a')).toBe('position: sticky; inset-inline-start: 0px; z-index: 30;')
|
|
61
|
+
expect(h.cellPinStyle('b')).toBe('position: sticky; inset-inline-start: 120px; z-index: 30;')
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
it('returns a right sticky style when the column has a right offset', () => {
|
|
65
65
|
const ctx = makeCtx({ pinnedOffsets: { left: {}, right: { d: 0 } } })
|
|
66
66
|
const h = createColumns(ctx)
|
|
67
|
-
expect(h.cellPinStyle('d')).toBe('position: sticky;
|
|
67
|
+
expect(h.cellPinStyle('d')).toBe('position: sticky; inset-inline-end: 0px; z-index: 30;')
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
it('returns empty string for an unpinned column', () => {
|
|
@@ -75,7 +75,7 @@ describe('cellPinStyle', () => {
|
|
|
75
75
|
it('prefers the left offset when a column appears in both maps', () => {
|
|
76
76
|
const ctx = makeCtx({ pinnedOffsets: { left: { a: 5 }, right: { a: 9 } } })
|
|
77
77
|
const h = createColumns(ctx)
|
|
78
|
-
expect(h.cellPinStyle('a')).toBe('position: sticky;
|
|
78
|
+
expect(h.cellPinStyle('a')).toBe('position: sticky; inset-inline-start: 5px; z-index: 30;')
|
|
79
79
|
})
|
|
80
80
|
})
|
|
81
81
|
|
package/src/columns.ts
CHANGED
|
@@ -16,13 +16,18 @@ export function createColumns<
|
|
|
16
16
|
// context (z=20) so a selected cell in the scrollable middle never
|
|
17
17
|
// bleeds over the pinned columns during horizontal scroll. Still
|
|
18
18
|
// sits below dropdowns (z=900+) and the column menu.
|
|
19
|
+
// Logical insets, not `left` / `right`: the offsets are measured from the
|
|
20
|
+
// pinned side in READING order, so under `dir="rtl"` the left-pinned
|
|
21
|
+
// columns have to stick to the right edge. `inset-inline-start` is that
|
|
22
|
+
// edge in either direction; with `left` a right-to-left grid scrolls its
|
|
23
|
+
// pinned columns off the screen.
|
|
19
24
|
const leftOffset = ctx.pinnedOffsets.left[columnId];
|
|
20
25
|
if (leftOffset !== undefined) {
|
|
21
|
-
return `position: sticky;
|
|
26
|
+
return `position: sticky; inset-inline-start: ${leftOffset}px; z-index: 30;`;
|
|
22
27
|
}
|
|
23
28
|
const rightOffset = ctx.pinnedOffsets.right[columnId];
|
|
24
29
|
if (rightOffset !== undefined) {
|
|
25
|
-
return `position: sticky;
|
|
30
|
+
return `position: sticky; inset-inline-end: ${rightOffset}px; z-index: 30;`;
|
|
26
31
|
}
|
|
27
32
|
return "";
|
|
28
33
|
}
|
|
@@ -83,6 +83,14 @@ describe('resolveCellFormat', () => {
|
|
|
83
83
|
expect(resolveCellFormat(1, {}, 'c', fmts, null).background).toBeUndefined()
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
+
it('rule carries its icon while it matches', () => {
|
|
87
|
+
const fmts: ConditionalFormat[] = [
|
|
88
|
+
{ type: 'rule', when: ({ value }) => Number(value) > 100, background: '#dbeafe', icon: '!' },
|
|
89
|
+
]
|
|
90
|
+
expect(resolveCellFormat(150, {}, 'c', fmts, null).icon).toBe('!')
|
|
91
|
+
expect(resolveCellFormat(50, {}, 'c', fmts, null).icon).toBeUndefined()
|
|
92
|
+
})
|
|
93
|
+
|
|
86
94
|
it('respects the columns scope', () => {
|
|
87
95
|
const fmts: ConditionalFormat[] = [
|
|
88
96
|
{ type: 'colorScale', min: '#000000', max: '#ffffff', columns: ['other'] },
|
|
@@ -102,6 +102,8 @@ export type RuleFormat<TData = unknown> = {
|
|
|
102
102
|
background?: string
|
|
103
103
|
color?: string
|
|
104
104
|
fontWeight?: string | number
|
|
105
|
+
/** A short glyph drawn before the value while the rule matches. */
|
|
106
|
+
icon?: string
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
export type ConditionalFormatSpec<TData = unknown> =
|
|
@@ -323,6 +325,7 @@ export function resolveCellFormat<TData = unknown>(
|
|
|
323
325
|
}
|
|
324
326
|
if (fmt.color) out.color = fmt.color
|
|
325
327
|
if (fmt.fontWeight != null) out.fontWeight = fmt.fontWeight
|
|
328
|
+
if (fmt.icon) out.icon = fmt.icon
|
|
326
329
|
}
|
|
327
330
|
continue
|
|
328
331
|
}
|
|
@@ -119,3 +119,54 @@ describe('row-model cache: selection', () => {
|
|
|
119
119
|
expect(target.getIsSelected()).toBe(false)
|
|
120
120
|
})
|
|
121
121
|
})
|
|
122
|
+
|
|
123
|
+
describe('base rows: reuse across a data swap', () => {
|
|
124
|
+
// The core reads `options.data` and `options.columns` off the object it was
|
|
125
|
+
// given (the component hands it a reactive one), so a swap is a write there.
|
|
126
|
+
const grid = () => {
|
|
127
|
+
const options = {
|
|
128
|
+
_features: tableFeatures({ rowSelectionFeature }),
|
|
129
|
+
_rowModels: { coreRowModel: createCoreRowModel<Row>() },
|
|
130
|
+
columns: COLUMNS as unknown as Array<ColumnDef<ReturnType<typeof tableFeatures>, Row>>,
|
|
131
|
+
data: makeRows(50),
|
|
132
|
+
getRowId: (r: Row) => String(r.id),
|
|
133
|
+
}
|
|
134
|
+
return { g: createSvGridCore<ReturnType<typeof tableFeatures>, Row>(options), options }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
it('keeps the row object for a data object that stayed at its index, and rebuilds the rest', () => {
|
|
138
|
+
const { g, options } = grid()
|
|
139
|
+
const before = g.getRowModel().rows
|
|
140
|
+
const data = options.data.slice()
|
|
141
|
+
// A block landing: forty of fifty entries are the same objects, ten are new.
|
|
142
|
+
for (let i = 20; i < 30; i += 1) data[i] = { ...data[i]!, name: 'fresh' }
|
|
143
|
+
options.data = data
|
|
144
|
+
const after = g.getRowModel().rows
|
|
145
|
+
expect(after).toHaveLength(50)
|
|
146
|
+
for (let i = 0; i < 50; i += 1) {
|
|
147
|
+
if (i >= 20 && i < 30) expect(after[i]).not.toBe(before[i])
|
|
148
|
+
else expect(after[i]).toBe(before[i])
|
|
149
|
+
}
|
|
150
|
+
expect(after[25]!.getCellValueByColumnId('name')).toBe('fresh')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('does not serve a memoised value after the object was changed in place', () => {
|
|
154
|
+
const { g, options } = grid()
|
|
155
|
+
const rows = g.getRowModel().rows
|
|
156
|
+
expect(rows[3]!.getCellValueByColumnId('score')).toBe((3 * 31) % 1000)
|
|
157
|
+
options.data[3]!.score = 4242
|
|
158
|
+
options.data = options.data.slice()
|
|
159
|
+
const again = g.getRowModel().rows
|
|
160
|
+
expect(again[3]).toBe(rows[3])
|
|
161
|
+
expect(again[3]!.getCellValueByColumnId('score')).toBe(4242)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('rebuilds every row when the columns change', () => {
|
|
165
|
+
const { g, options } = grid()
|
|
166
|
+
const before = g.getRowModel().rows
|
|
167
|
+
options.columns = [...COLUMNS, { field: 'extra' }] as unknown as typeof options.columns
|
|
168
|
+
const after = g.getRowModel().rows
|
|
169
|
+
expect(after[0]).not.toBe(before[0])
|
|
170
|
+
expect(after[0]!.getAllCells()).toHaveLength(4)
|
|
171
|
+
})
|
|
172
|
+
})
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tick - a new data array where a few row objects were replaced - repairs
|
|
3
|
+
* the filtered and sorted stages' previous output instead of recomputing it,
|
|
4
|
+
* and the repaired output is exactly what the full pipeline would produce.
|
|
5
|
+
*
|
|
6
|
+
* The full pipeline is the oracle: for every case below a second grid is
|
|
7
|
+
* built fresh from the ticked data and its rows compared, object by object
|
|
8
|
+
* (same `original`, same order), against the ticked grid's rows. Sort keys
|
|
9
|
+
* of every kind the sorted stage specialises (number, date, ranked text,
|
|
10
|
+
* unranked text, a custom comparator), ties, descending, several clauses,
|
|
11
|
+
* an active filter, and the cases that must NOT repair: an add, a remove, a
|
|
12
|
+
* replacement whose filter membership flips, and a text value the ranking
|
|
13
|
+
* has not seen.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, expect, it } from 'vitest'
|
|
16
|
+
import {
|
|
17
|
+
createCoreRowModel,
|
|
18
|
+
createFilteredRowModel,
|
|
19
|
+
createSortedRowModel,
|
|
20
|
+
createSvGridCore,
|
|
21
|
+
rowSortingFeature,
|
|
22
|
+
columnFilteringFeature,
|
|
23
|
+
tableFeatures,
|
|
24
|
+
sortFns,
|
|
25
|
+
type ColumnDef,
|
|
26
|
+
type SortingState,
|
|
27
|
+
type ColumnFiltersState,
|
|
28
|
+
} from './core'
|
|
29
|
+
|
|
30
|
+
type Quote = { id: number; symbol: string; sector: string; price: number; asOf: string; note: string }
|
|
31
|
+
|
|
32
|
+
const SECTORS = ['Tech', 'Energy', 'Health', 'Finance']
|
|
33
|
+
function makeRows(n: number, seed = 1): Quote[] {
|
|
34
|
+
let s = seed
|
|
35
|
+
const rand = () => (s = (s * 1664525 + 1013904223) >>> 0) / 0xffffffff
|
|
36
|
+
return Array.from({ length: n }, (_, i) => ({
|
|
37
|
+
id: i,
|
|
38
|
+
symbol: `S${String(i).padStart(4, '0')}`,
|
|
39
|
+
sector: SECTORS[Math.floor(rand() * SECTORS.length)]!,
|
|
40
|
+
// Many ties on purpose: the stable tie-break is the thing to get right.
|
|
41
|
+
price: Math.round(rand() * 50),
|
|
42
|
+
asOf: new Date(Date.UTC(2026, 0, 1 + Math.floor(rand() * 200))).toISOString(),
|
|
43
|
+
note: `n${Math.floor(rand() * 1e6)}`,
|
|
44
|
+
}))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
|
|
48
|
+
const COLUMNS = [
|
|
49
|
+
{ field: 'id', editorType: 'number' },
|
|
50
|
+
{ field: 'symbol' },
|
|
51
|
+
{ field: 'sector' },
|
|
52
|
+
{ field: 'price', editorType: 'number' },
|
|
53
|
+
{ field: 'asOf', editorType: 'date' },
|
|
54
|
+
{ field: 'note' },
|
|
55
|
+
] as unknown as Array<ColumnDef<typeof features, Quote>>
|
|
56
|
+
|
|
57
|
+
function makeGrid(
|
|
58
|
+
data: Quote[],
|
|
59
|
+
state: { sorting?: SortingState; columnFilters?: ColumnFiltersState },
|
|
60
|
+
fns: typeof sortFns = sortFns,
|
|
61
|
+
) {
|
|
62
|
+
let sortRuns = 0
|
|
63
|
+
const sorted = createSortedRowModel<Quote>(fns)
|
|
64
|
+
// A live getter, as <SvGrid> passes it: getRowModel reads options.data on
|
|
65
|
+
// every call, so replacing the array here is what a tick looks like.
|
|
66
|
+
const ref = { data }
|
|
67
|
+
const grid = createSvGridCore<typeof features, Quote>({
|
|
68
|
+
_features: features,
|
|
69
|
+
_rowModels: {
|
|
70
|
+
coreRowModel: createCoreRowModel(),
|
|
71
|
+
filteredRowModel: createFilteredRowModel(),
|
|
72
|
+
sortedRowModel: (args) => {
|
|
73
|
+
sortRuns++
|
|
74
|
+
return sorted(args)
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
columns: COLUMNS,
|
|
78
|
+
get data() {
|
|
79
|
+
return ref.data
|
|
80
|
+
},
|
|
81
|
+
state: { sorting: [], columnFilters: [], ...state },
|
|
82
|
+
})
|
|
83
|
+
return { grid, sortRuns: () => sortRuns, set: (next: Quote[]) => (ref.data = next) }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The ticked data: the same array with `count` rows replaced by new objects. */
|
|
87
|
+
function tick(data: Quote[], count: number, mutate: (q: Quote, i: number) => Partial<Quote>, seed = 7): Quote[] {
|
|
88
|
+
let s = seed
|
|
89
|
+
const rand = () => (s = (s * 1664525 + 1013904223) >>> 0) / 0xffffffff
|
|
90
|
+
const next = data.slice()
|
|
91
|
+
const picked = new Set<number>()
|
|
92
|
+
while (picked.size < count) picked.add(Math.floor(rand() * data.length))
|
|
93
|
+
for (const i of picked) next[i] = { ...data[i]!, ...mutate(data[i]!, i) }
|
|
94
|
+
return next
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function originals(grid: ReturnType<typeof makeGrid>['grid']) {
|
|
98
|
+
return grid.getRowModel().rows.map((r) => r.original)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function expectRepairedEqualsFull(
|
|
102
|
+
before: Quote[],
|
|
103
|
+
after: Quote[],
|
|
104
|
+
state: { sorting?: SortingState; columnFilters?: ColumnFiltersState },
|
|
105
|
+
fns?: typeof sortFns,
|
|
106
|
+
) {
|
|
107
|
+
const ticked = makeGrid(before, state, fns)
|
|
108
|
+
ticked.grid.getRowModel()
|
|
109
|
+
ticked.set(after)
|
|
110
|
+
const repaired = originals(ticked.grid)
|
|
111
|
+
|
|
112
|
+
const oracle = makeGrid(after, state, fns)
|
|
113
|
+
const full = originals(oracle.grid)
|
|
114
|
+
|
|
115
|
+
expect(repaired.length).toBe(full.length)
|
|
116
|
+
for (let i = 0; i < full.length; i++) {
|
|
117
|
+
if (repaired[i] !== full[i]) {
|
|
118
|
+
throw new Error(`row ${i} differs: repaired id=${repaired[i]?.id} price=${repaired[i]?.price}, full id=${full[i]?.id} price=${full[i]?.price}`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return ticked
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
describe('tick repair: sorted stage equals a full sort', () => {
|
|
125
|
+
const price = (q: Quote) => ({ price: (q.price * 7 + 13) % 50 })
|
|
126
|
+
|
|
127
|
+
it('numeric ascending, with ties', () => {
|
|
128
|
+
const before = makeRows(2000)
|
|
129
|
+
expectRepairedEqualsFull(before, tick(before, 150, price), { sorting: [{ id: 'price', desc: false }] })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('numeric descending', () => {
|
|
133
|
+
const before = makeRows(2000)
|
|
134
|
+
expectRepairedEqualsFull(before, tick(before, 150, price), { sorting: [{ id: 'price', desc: true }] })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('date column', () => {
|
|
138
|
+
const before = makeRows(1500)
|
|
139
|
+
expectRepairedEqualsFull(
|
|
140
|
+
before,
|
|
141
|
+
tick(before, 90, (q) => ({ asOf: new Date(Date.parse(q.asOf) + 86_400_000 * 37).toISOString() })),
|
|
142
|
+
{ sorting: [{ id: 'asOf', desc: false }] },
|
|
143
|
+
)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('ranked text (few distinct values) when the new value is a known one', () => {
|
|
147
|
+
const before = makeRows(2000)
|
|
148
|
+
expectRepairedEqualsFull(
|
|
149
|
+
before,
|
|
150
|
+
tick(before, 120, (q) => ({ sector: SECTORS[(SECTORS.indexOf(q.sector) + 1) % SECTORS.length]! })),
|
|
151
|
+
{ sorting: [{ id: 'sector', desc: false }, { id: 'price', desc: true }] },
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('unranked text (mostly distinct values)', () => {
|
|
156
|
+
const before = makeRows(1200)
|
|
157
|
+
expectRepairedEqualsFull(before, tick(before, 60, (q, i) => ({ note: `z${i}-${q.note}` })), {
|
|
158
|
+
sorting: [{ id: 'note', desc: false }],
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('a custom comparator', () => {
|
|
163
|
+
const before = makeRows(800)
|
|
164
|
+
const custom = { ...sortFns, number: (a: unknown, b: unknown) => (Number(b) % 10) - (Number(a) % 10) }
|
|
165
|
+
expectRepairedEqualsFull(before, tick(before, 40, price), { sorting: [{ id: 'price', desc: false }] }, custom)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('three clauses', () => {
|
|
169
|
+
const before = makeRows(3000)
|
|
170
|
+
expectRepairedEqualsFull(before, tick(before, 200, price), {
|
|
171
|
+
sorting: [{ id: 'sector', desc: true }, { id: 'price', desc: false }, { id: 'id', desc: true }],
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('a replacement that leaves the key unchanged keeps its slot', () => {
|
|
176
|
+
const before = makeRows(500)
|
|
177
|
+
expectRepairedEqualsFull(before, tick(before, 30, (q) => ({ note: q.note + '!' })), {
|
|
178
|
+
sorting: [{ id: 'price', desc: false }],
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('back-to-back ticks repair on top of a repair', () => {
|
|
183
|
+
const before = makeRows(2000)
|
|
184
|
+
const t1 = tick(before, 100, price, 3)
|
|
185
|
+
const t2 = tick(t1, 100, price, 5)
|
|
186
|
+
const t3 = tick(t2, 100, price, 9)
|
|
187
|
+
const g = makeGrid(before, { sorting: [{ id: 'price', desc: true }] })
|
|
188
|
+
g.grid.getRowModel()
|
|
189
|
+
for (const d of [t1, t2, t3]) g.set(d)
|
|
190
|
+
const full = originals(makeGrid(t3, { sorting: [{ id: 'price', desc: true }] }).grid)
|
|
191
|
+
expect(originals(g.grid)).toEqual(full)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('tick repair: filtered stage', () => {
|
|
196
|
+
const filters: ColumnFiltersState = [{ id: 'sector', value: 'Tech' }]
|
|
197
|
+
|
|
198
|
+
it('keeps a replaced row in its slot when it still passes', () => {
|
|
199
|
+
const before = makeRows(2000)
|
|
200
|
+
const g = expectRepairedEqualsFull(
|
|
201
|
+
before,
|
|
202
|
+
tick(before, 100, (q) => ({ price: q.price + 1 })),
|
|
203
|
+
{ columnFilters: filters, sorting: [{ id: 'price', desc: false }] },
|
|
204
|
+
)
|
|
205
|
+
expect(g.sortRuns()).toBe(2)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('falls back to the full pass when a replacement changes membership', () => {
|
|
209
|
+
const before = makeRows(2000)
|
|
210
|
+
expectRepairedEqualsFull(
|
|
211
|
+
before,
|
|
212
|
+
tick(before, 50, (q) => ({ sector: q.sector === 'Tech' ? 'Energy' : 'Tech' })),
|
|
213
|
+
{ columnFilters: filters, sorting: [{ id: 'price', desc: false }] },
|
|
214
|
+
)
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
describe('tick repair: when it must not happen', () => {
|
|
219
|
+
it('an add goes through the full pipeline', () => {
|
|
220
|
+
const before = makeRows(1000)
|
|
221
|
+
const after = [...before, { ...before[0]!, id: 1000, symbol: 'S1000', price: 25 }]
|
|
222
|
+
expectRepairedEqualsFull(before, after, { sorting: [{ id: 'price', desc: false }] })
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('a remove goes through the full pipeline', () => {
|
|
226
|
+
const before = makeRows(1000)
|
|
227
|
+
const after = before.filter((_, i) => i !== 17)
|
|
228
|
+
expectRepairedEqualsFull(before, after, { sorting: [{ id: 'price', desc: false }] })
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('a ranked text column with a value the ranking has not seen', () => {
|
|
232
|
+
const before = makeRows(2000)
|
|
233
|
+
expectRepairedEqualsFull(before, tick(before, 10, () => ({ sector: 'Utilities' })), {
|
|
234
|
+
sorting: [{ id: 'sector', desc: false }],
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('more than a quarter of the rows replaced', () => {
|
|
239
|
+
const before = makeRows(1000)
|
|
240
|
+
expectRepairedEqualsFull(before, tick(before, 400, (q) => ({ price: q.price + 3 })), {
|
|
241
|
+
sorting: [{ id: 'price', desc: false }],
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('a sorting change between ticks', () => {
|
|
246
|
+
const before = makeRows(1000)
|
|
247
|
+
const g = makeGrid(before, { sorting: [{ id: 'price', desc: false }] })
|
|
248
|
+
g.grid.getRowModel()
|
|
249
|
+
const after = tick(before, 50, (q) => ({ price: q.price + 5 }))
|
|
250
|
+
g.grid.store.setState((prev) => ({ ...prev, sorting: [{ id: 'price', desc: true }] }))
|
|
251
|
+
g.set(after)
|
|
252
|
+
expect(originals(g.grid)).toEqual(originals(makeGrid(after, { sorting: [{ id: 'price', desc: true }] }).grid))
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('a new array with every object the same re-reads the rows (the in-place refresh idiom)', () => {
|
|
256
|
+
// `rows = [...rows]` after mutating objects in place is the documented
|
|
257
|
+
// refresh (docs/help/cells/view-refresh.md). Nothing was replaced, so
|
|
258
|
+
// there is no repair; the stages recompute from the current values and
|
|
259
|
+
// the mutation is picked up.
|
|
260
|
+
const before = makeRows(200)
|
|
261
|
+
const g = makeGrid(before, { sorting: [{ id: 'price', desc: false }] })
|
|
262
|
+
g.grid.getRowModel()
|
|
263
|
+
before[0]!.price = 999
|
|
264
|
+
g.set(before.slice())
|
|
265
|
+
const rows = originals(g.grid)
|
|
266
|
+
expect(rows[rows.length - 1]).toBe(before[0])
|
|
267
|
+
expect(g.sortRuns()).toBe(2)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('a kept row is assumed unchanged in value: the immutable-update contract', () => {
|
|
271
|
+
// Replace one object and mutate another in place in the same tick. The
|
|
272
|
+
// replaced one lands in sorted position; the mutated one keeps the slot
|
|
273
|
+
// its old key earned, because the repair only reads the rows it was told
|
|
274
|
+
// changed. A feed that mutates in place must not mix in replacements, or
|
|
275
|
+
// must pass a new array with no replacements to refresh.
|
|
276
|
+
const before = makeRows(200)
|
|
277
|
+
const g = makeGrid(before, { sorting: [{ id: 'price', desc: false }] })
|
|
278
|
+
g.grid.getRowModel()
|
|
279
|
+
const next = before.slice()
|
|
280
|
+
next[5] = { ...before[5]!, price: 999 }
|
|
281
|
+
before[7]!.price = -1
|
|
282
|
+
g.set(next)
|
|
283
|
+
const rows = originals(g.grid)
|
|
284
|
+
expect(rows[rows.length - 1]).toBe(next[5])
|
|
285
|
+
expect(rows[0]).not.toBe(before[7])
|
|
286
|
+
})
|
|
287
|
+
})
|