@svgrid/grid 3.0.4 → 4.0.0
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 +3 -3
- package/dist/SvGrid.controller.svelte.d.ts +12 -2
- package/dist/SvGrid.controller.svelte.js +191 -8
- package/dist/SvGrid.css +73 -0
- package/dist/SvGrid.svelte +82 -3
- package/dist/SvGrid.types.d.ts +105 -3
- package/dist/build-api.js +11 -6
- package/dist/cdn/{GridMenus-7kbpnnBW.js → GridMenus-CLI93jke.js} +26 -26
- package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-CwooL3QA.js} +26 -26
- package/dist/cdn/server-block-cache-CGoWz-87.js +237 -0
- package/dist/cdn/{src-BKhZ6eXd.js → src-B6HXdfHY.js} +4099 -4140
- package/dist/cdn/{src-V1uu8iE9.js → src-C6Iowdvs.js} +4052 -4093
- package/dist/cdn/svgrid.js +27 -26
- package/dist/cdn/svgrid.svelte-external.js +27 -26
- package/dist/cdn/validate-CchXzyrX.js +76 -0
- package/dist/core.js +36 -13
- package/dist/editing.js +10 -0
- package/dist/grid-icons.d.ts +2 -2
- package/dist/grid-icons.js +2 -2
- package/dist/grid-messages.d.ts +2 -0
- package/dist/grid-messages.js +2 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +2 -3
- package/dist/row-model.d.ts +133 -0
- package/dist/row-model.js +16 -0
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +30 -2
- package/dist/server-block-cache.d.ts +214 -0
- package/dist/server-block-cache.js +531 -0
- package/dist/server-data-source.d.ts +240 -6
- package/dist/server-data-source.js +220 -20
- package/dist/server.d.ts +13 -0
- package/dist/server.js +13 -0
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +8 -3
- package/package.json +6 -1
- package/src/SvGrid.controller.svelte.ts +190 -8
- package/src/SvGrid.css +73 -0
- package/src/SvGrid.svelte +82 -3
- package/src/SvGrid.types.ts +105 -3
- package/src/build-api.ts +11 -4
- package/src/core.rowmodel-cache.test.ts +51 -0
- package/src/core.ts +33 -12
- package/src/editing.ts +8 -0
- package/src/grid-icons.ts +2 -2
- package/src/grid-messages.ts +5 -0
- package/src/icon-seam.test.ts +2 -4
- package/src/index.ts +19 -10
- package/src/row-model.ts +146 -0
- package/src/selection.test.ts +3 -0
- package/src/selection.ts +32 -2
- package/src/server-block-cache.test.ts +645 -0
- package/src/server-block-cache.ts +677 -0
- package/src/server-data-source.infinite.test.ts +343 -0
- package/src/server-data-source.ts +450 -27
- package/src/server.ts +47 -0
- package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +289 -0
- package/src/validate.test.ts +10 -0
- package/src/validate.ts +10 -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/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/SvGrid.types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Type definitions extracted from SvGrid.svelte. These are compile-time
|
|
2
2
|
// only - moving them out keeps the component's <script> focused on logic.
|
|
3
3
|
import type { Snippet } from "svelte";
|
|
4
|
+
import type { GridRowModel } from "./row-model";
|
|
4
5
|
import type {
|
|
5
6
|
CellEditorType,
|
|
6
7
|
ColumnDef,
|
|
@@ -1113,7 +1114,13 @@ export type SchedulerConfig<
|
|
|
1113
1114
|
};
|
|
1114
1115
|
|
|
1115
1116
|
export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData> = {
|
|
1116
|
-
|
|
1117
|
+
/**
|
|
1118
|
+
* The rows to render.
|
|
1119
|
+
*
|
|
1120
|
+
* Optional only because `rowModel` can supply them instead; a grid with
|
|
1121
|
+
* neither renders empty. When both are present `data` wins.
|
|
1122
|
+
*/
|
|
1123
|
+
data?: ReadonlyArray<TData>;
|
|
1117
1124
|
columns: Array<ColumnDef<TFeatures, TData>>;
|
|
1118
1125
|
/**
|
|
1119
1126
|
* Kanban board mode. When set, the grid renders its rows as cards in
|
|
@@ -2099,6 +2106,99 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
2099
2106
|
scrollHeight: number;
|
|
2100
2107
|
clientHeight: number;
|
|
2101
2108
|
}) => void;
|
|
2109
|
+
/**
|
|
2110
|
+
* Fires when the range of rows on screen changes, with the first and last
|
|
2111
|
+
* row INDEX (not pixels). Coalesced to one call per frame, so it is cheap
|
|
2112
|
+
* to wire to something that fetches.
|
|
2113
|
+
*
|
|
2114
|
+
* This is the hook a block-loading data source needs and
|
|
2115
|
+
* `onScrollBottomReached` cannot give it: "the user is looking at rows
|
|
2116
|
+
* 4,000-4,020" answers which block to fetch and which to keep, while
|
|
2117
|
+
* "they hit the bottom" only ever means "append more". Both are free; use
|
|
2118
|
+
* this one with `createServerDataSource({ mode: 'infinite' })`, or let
|
|
2119
|
+
* `rowModel` wire it for you.
|
|
2120
|
+
*
|
|
2121
|
+
* Reports `0, data.length - 1` when virtualization is off, since every row
|
|
2122
|
+
* really is rendered.
|
|
2123
|
+
*/
|
|
2124
|
+
onVisibleRangeChange?: (range: {
|
|
2125
|
+
startIndex: number;
|
|
2126
|
+
endIndex: number;
|
|
2127
|
+
}) => void;
|
|
2128
|
+
/**
|
|
2129
|
+
* Marks a row as one whose data has not arrived: `"loading"` draws a
|
|
2130
|
+
* shimmer in every cell, `"failed"` draws a full-width "could not load"
|
|
2131
|
+
* row with a Retry button, and `null` (the default for every row) renders
|
|
2132
|
+
* normally.
|
|
2133
|
+
*
|
|
2134
|
+
* Placeholder rows are inert - not selectable, not editable, and skipped
|
|
2135
|
+
* by cell navigation - because there is nothing there to act on yet.
|
|
2136
|
+
*
|
|
2137
|
+
* `createServerDataSource` in `infinite` mode fills the gaps with rows this
|
|
2138
|
+
* recognises, so the usual wiring is `rowPlaceholder={rowPlaceholderState}`
|
|
2139
|
+
* (or nothing at all, via `rowModel`).
|
|
2140
|
+
*/
|
|
2141
|
+
rowPlaceholder?: (row: TData, rowIndex: number) => "loading" | "failed" | null;
|
|
2142
|
+
/** Called by the Retry button on a `"failed"` placeholder row. */
|
|
2143
|
+
onRetryRow?: (row: TData, rowIndex: number) => void;
|
|
2144
|
+
/**
|
|
2145
|
+
* Hand selection over to an external model.
|
|
2146
|
+
*
|
|
2147
|
+
* The grid normally tracks selection as a record of the row ids it has
|
|
2148
|
+
* seen, which is right until the rows it has seen are a window onto a
|
|
2149
|
+
* million on a server: "select all" then means 20 ticked checkboxes rather
|
|
2150
|
+
* than a million, and the header checkbox cannot honestly say `all`. A
|
|
2151
|
+
* model that stores the RULE ("everything except these three") can answer
|
|
2152
|
+
* both, so when this is set the header checkbox, the row checkboxes and
|
|
2153
|
+
* `api.selectAllRows()` all route through it instead.
|
|
2154
|
+
*
|
|
2155
|
+
* `@svgrid/enterprise` ships one for the server-side row model; this is the
|
|
2156
|
+
* seam it plugs into.
|
|
2157
|
+
*/
|
|
2158
|
+
/**
|
|
2159
|
+
* Drive the grid from a row model instead of wiring a dozen props.
|
|
2160
|
+
*
|
|
2161
|
+
* `createServerDataSource` returns one, and so does the Enterprise
|
|
2162
|
+
* server-side row model, so server-backed grids become:
|
|
2163
|
+
*
|
|
2164
|
+
* ```svelte
|
|
2165
|
+
* <SvGrid rowModel={ctl} {columns} />
|
|
2166
|
+
* ```
|
|
2167
|
+
*
|
|
2168
|
+
* The model supplies `data`, `loading`, `getRowId`, the external sort
|
|
2169
|
+
* and filter wiring, the visible range, placeholder rows, group
|
|
2170
|
+
* accessors, selection and paging - each one only if it implements that
|
|
2171
|
+
* part. A prop written explicitly on the grid always wins, so you can
|
|
2172
|
+
* adopt it and still override one piece.
|
|
2173
|
+
*/
|
|
2174
|
+
rowModel?: GridRowModel<TData>;
|
|
2175
|
+
/**
|
|
2176
|
+
* Columns that REPLACE `columns` while set. The server-side row model
|
|
2177
|
+
* supplies them in pivot mode - one column per pivoted value, grouped
|
|
2178
|
+
* under a header per pivot key - and clears them when pivot mode ends,
|
|
2179
|
+
* so `columns` stays the app's own list. Rarely set by hand.
|
|
2180
|
+
*/
|
|
2181
|
+
pivotResultColumns?: Array<ColumnDef<TFeatures, TData>> | null;
|
|
2182
|
+
rowSelectionModel?: {
|
|
2183
|
+
isSelected: (rowId: string, row: TData) => boolean;
|
|
2184
|
+
/** Whether the header checkbox shows empty, indeterminate, or ticked. */
|
|
2185
|
+
headerState: () => "none" | "some" | "all";
|
|
2186
|
+
toggle: (rowId: string, row: TData, next: boolean) => void;
|
|
2187
|
+
/** The header checkbox: select or clear everything, loaded or not. */
|
|
2188
|
+
toggleAll: (next: boolean) => void;
|
|
2189
|
+
/**
|
|
2190
|
+
* How many rows the rule selects, counting the ones the grid never
|
|
2191
|
+
* loaded; `null` when it cannot say. The selection bar shows this
|
|
2192
|
+
* instead of counting ticked rows on screen.
|
|
2193
|
+
*/
|
|
2194
|
+
selectedCount?: () => number | null;
|
|
2195
|
+
/**
|
|
2196
|
+
* Apply one patch to every selected row, loaded or not. When present,
|
|
2197
|
+
* the bulk-edit drawer sends its edits here instead of writing the
|
|
2198
|
+
* loaded cells. Resolves with how many rows changed.
|
|
2199
|
+
*/
|
|
2200
|
+
bulkUpdate?: (patch: Record<string, unknown>) => Promise<number>;
|
|
2201
|
+
};
|
|
2102
2202
|
/**
|
|
2103
2203
|
* Marks a row as an expandable "detail row". When this returns true the
|
|
2104
2204
|
* grid renders that row as a SINGLE full-width cell (colspan across every
|
|
@@ -2119,8 +2219,10 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
2119
2219
|
* Server-side group / tree keyboard + accessibility, built into the grid. When
|
|
2120
2220
|
* set, the grid uses the treegrid role and marks matching rows with
|
|
2121
2221
|
* `aria-level` / `aria-expanded`, and ArrowRight / ArrowLeft expand / collapse
|
|
2122
|
-
* the focused group row (no app-level key handling). Pair with
|
|
2123
|
-
* + `SvGroupCell` for the visual
|
|
2222
|
+
* the focused group row (no app-level key handling). Pair with
|
|
2223
|
+
* `serverGroupRows` + `SvGroupCell` from `@svgrid/enterprise` for the visual
|
|
2224
|
+
* expander, or drive it from your own tree state. Every accessor receives the
|
|
2225
|
+
* row data.
|
|
2124
2226
|
*/
|
|
2125
2227
|
serverGroup?: {
|
|
2126
2228
|
/** Whether a row is an expandable group / branch. */
|
package/src/build-api.ts
CHANGED
|
@@ -735,9 +735,10 @@ export function createGridApi<
|
|
|
735
735
|
});
|
|
736
736
|
},
|
|
737
737
|
selectAllRows() {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
738
|
+
// One implementation of "select everything", shared with the header
|
|
739
|
+
// checkbox, so it knows about an external selection model and about
|
|
740
|
+
// rows whose data has not arrived.
|
|
741
|
+
ctx.setSelectAllRows(true);
|
|
741
742
|
},
|
|
742
743
|
toggleRowSelected(id) {
|
|
743
744
|
ctx.toggleRowSelectionById(id);
|
|
@@ -745,7 +746,13 @@ export function createGridApi<
|
|
|
745
746
|
// ---- Pagination
|
|
746
747
|
getPageInfo() {
|
|
747
748
|
const { pageIndex, pageSize } = ctx.paginationState;
|
|
748
|
-
|
|
749
|
+
// Under external pagination the grid holds ONE page, so counting the
|
|
750
|
+
// rows in hand would report a 95-row table as a 20-row one. The
|
|
751
|
+
// consumer tells us the real total through rowCount, which is what
|
|
752
|
+
// the footer has always shown.
|
|
753
|
+
const total = ctx.externalPaginationEnabled
|
|
754
|
+
? (ctx.props.rowCount ?? ctx.allRowsBeforePagination.length)
|
|
755
|
+
: ctx.allRowsBeforePagination.length;
|
|
749
756
|
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
|
|
750
757
|
return { pageIndex, pageSize, pageCount, total };
|
|
751
758
|
},
|
|
@@ -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
|
+
})
|
package/src/core.ts
CHANGED
|
@@ -1545,6 +1545,7 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1545
1545
|
let cachedBaseRowsInput: ReadonlyArray<TData> | null = null
|
|
1546
1546
|
let cachedBaseRowsColumns: Array<Column<TData>> | null = null
|
|
1547
1547
|
let cachedBaseRows: Array<Row<TData>> = []
|
|
1548
|
+
let cachedRowCtx: BaseRowCtx<TData> | null = null
|
|
1548
1549
|
let cachedRowModel: RowModel<TData> | null = null
|
|
1549
1550
|
let cachedRowModelBaseRows: Array<Row<TData>> | null = null
|
|
1550
1551
|
let cachedPipeline = options._rowModels
|
|
@@ -1754,22 +1755,35 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1754
1755
|
getRowModel() {
|
|
1755
1756
|
const columns = grid.getAllColumns()
|
|
1756
1757
|
if (cachedBaseRowsInput !== options.data || cachedBaseRowsColumns !== columns) {
|
|
1758
|
+
// Same columns as last time: the shared context still describes them,
|
|
1759
|
+
// and a row whose data object sits at the same index can keep its
|
|
1760
|
+
// row object. A row model that streams blocks hands the grid a new
|
|
1761
|
+
// array of 60k entries per block where 100 changed; building 60k
|
|
1762
|
+
// fresh row objects each time was most of the cost of a block
|
|
1763
|
+
// landing. The reused row drops its memoised values and cells, since
|
|
1764
|
+
// an app may have changed the object in place before passing a new
|
|
1765
|
+
// array - that is the case the old rebuild covered by accident.
|
|
1766
|
+
const previous = cachedBaseRowsColumns === columns && cachedRowCtx ? cachedBaseRows : null
|
|
1757
1767
|
cachedBaseRowsInput = options.data
|
|
1758
1768
|
cachedBaseRowsColumns = columns
|
|
1759
1769
|
// O(1) column-id → index lookup so getCellValueByColumnId doesn't do
|
|
1760
1770
|
// a linear `findIndex` on every cell read (was O(rows × cells × cols)).
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1771
|
+
let rowCtx: BaseRowCtx<TData>
|
|
1772
|
+
if (previous && cachedRowCtx) {
|
|
1773
|
+
rowCtx = cachedRowCtx
|
|
1774
|
+
} else {
|
|
1775
|
+
const columnIndexById = new Map<string, number>()
|
|
1776
|
+
for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)
|
|
1777
|
+
// One shared context for every row in this table, so a row carries a
|
|
1778
|
+
// pointer rather than a closure scope. See BASE_ROW_METHODS.
|
|
1779
|
+
rowCtx = {
|
|
1780
|
+
grid: grid as SvGrid<TData>,
|
|
1781
|
+
store,
|
|
1782
|
+
columns,
|
|
1783
|
+
columnCount: columns.length,
|
|
1784
|
+
columnIndexById,
|
|
1785
|
+
}
|
|
1786
|
+
cachedRowCtx = rowCtx
|
|
1773
1787
|
}
|
|
1774
1788
|
|
|
1775
1789
|
cachedBaseRows = new Array(options.data.length)
|
|
@@ -1785,6 +1799,13 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1785
1799
|
}
|
|
1786
1800
|
for (let index = 0; index < options.data.length; index++) {
|
|
1787
1801
|
const original = options.data[index]!
|
|
1802
|
+
const kept = previous?.[index] as BaseRowState<TData> | undefined
|
|
1803
|
+
if (kept && kept.original === original) {
|
|
1804
|
+
kept[ROW_VALUES] = null
|
|
1805
|
+
kept[ROW_CELLS] = null
|
|
1806
|
+
cachedBaseRows[index] = kept
|
|
1807
|
+
continue
|
|
1808
|
+
}
|
|
1788
1809
|
// `_values` and `_cells` stay null until something reads them - a
|
|
1789
1810
|
// 100k-row grid showing twenty rows must not materialise every row's
|
|
1790
1811
|
// values or cell objects to paint.
|
package/src/editing.ts
CHANGED
|
@@ -47,6 +47,14 @@ export function createEditing<
|
|
|
47
47
|
function isCellEditable(column: Column<TData>, row?: Row<TData>): boolean {
|
|
48
48
|
const editable = column.columnDef.editable;
|
|
49
49
|
if (editable === false) return false;
|
|
50
|
+
if (row) {
|
|
51
|
+
// A server-side group row is a key with aggregates under the leaf
|
|
52
|
+
// columns, and a placeholder row has no data yet: neither takes an
|
|
53
|
+
// edit, whatever the column says.
|
|
54
|
+
const serverGroup = ctx.props.serverGroup;
|
|
55
|
+
if (serverGroup && row.original !== undefined && serverGroup.isGroup(row.original)) return false;
|
|
56
|
+
if (ctx.placeholderStateOf?.(row)) return false;
|
|
57
|
+
}
|
|
50
58
|
if (typeof editable !== "function") return true;
|
|
51
59
|
if (!row) return true;
|
|
52
60
|
const cellCtx: CellContext<TData> = {
|
package/src/grid-icons.ts
CHANGED
|
@@ -97,8 +97,8 @@ export type GridIcons = Partial<Record<GridIconName, Snippet>>
|
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* Icons whose built-in form is a character rather than an SVG path. Kept as
|
|
100
|
-
* data so `<SvGrid>`, its footer and the
|
|
101
|
-
* the same defaults instead of three copies drifting apart.
|
|
100
|
+
* data so `<SvGrid>`, its footer and the row-group panel in @svgrid/enterprise
|
|
101
|
+
* render the same defaults instead of three copies drifting apart.
|
|
102
102
|
*
|
|
103
103
|
* Each is the exact character that shipped inline before, so a grid that sets
|
|
104
104
|
* no `icons` renders byte-for-byte what it always did.
|
package/src/grid-messages.ts
CHANGED
|
@@ -106,6 +106,9 @@ export type GridChromeMessages = {
|
|
|
106
106
|
bulkEditCancel: string
|
|
107
107
|
bulkEditNoFields: string
|
|
108
108
|
chartRange: string
|
|
109
|
+
// Rows whose data has not arrived (server-side / infinite loading)
|
|
110
|
+
rowLoadFailed: string
|
|
111
|
+
rowLoadRetry: string
|
|
109
112
|
// Upsell notes (enterprise views not installed)
|
|
110
113
|
pivotUpsellTitle: string
|
|
111
114
|
pivotUpsellBody: string
|
|
@@ -185,6 +188,8 @@ export const defaultGridMessages: GridChromeMessages = {
|
|
|
185
188
|
opBefore: 'Before',
|
|
186
189
|
opAfter: 'After',
|
|
187
190
|
chartRange: 'Chart selected range',
|
|
191
|
+
rowLoadFailed: 'Could not load these rows.',
|
|
192
|
+
rowLoadRetry: 'Retry',
|
|
188
193
|
pivotUpsellTitle: 'Pivot mode',
|
|
189
194
|
pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',
|
|
190
195
|
upsellLicense:
|
package/src/icon-seam.test.ts
CHANGED
|
@@ -20,18 +20,16 @@ const SRC = join(process.cwd(), 'src')
|
|
|
20
20
|
const read = (f: string) => readFileSync(join(SRC, f), 'utf8')
|
|
21
21
|
|
|
22
22
|
/** Files that render grid chrome and therefore must go through the seam. */
|
|
23
|
-
const CHROME = ['SvGrid.svelte', 'GridMenus.svelte', 'GridFooter.svelte'
|
|
23
|
+
const CHROME = ['SvGrid.svelte', 'GridMenus.svelte', 'GridFooter.svelte']
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Drop the regions allowed to contain raw icon markup: the `icon` snippet's own
|
|
27
|
-
* body (which IS the built-in set)
|
|
28
|
-
* every `<script>` block.
|
|
27
|
+
* body (which IS the built-in set) and every `<script>` block.
|
|
29
28
|
*/
|
|
30
29
|
function stripAllowedRegions(src: string): string {
|
|
31
30
|
let out = src.replace(/<script[\s\S]*?<\/script>/g, '')
|
|
32
31
|
// The built-in catalogue: from the snippet header to its closing tag.
|
|
33
32
|
out = out.replace(/\{#snippet icon\(name: GridIconName\)\}[\s\S]*?\n\{\/snippet\}/g, '')
|
|
34
|
-
out = out.replace(/\{#snippet ic\(name: GridIconName\)\}[\s\S]*?\{\/snippet\}/g, '')
|
|
35
33
|
return out
|
|
36
34
|
}
|
|
37
35
|
|
package/src/index.ts
CHANGED
|
@@ -754,9 +754,11 @@ export {
|
|
|
754
754
|
export {
|
|
755
755
|
createServerDataSource,
|
|
756
756
|
type ServerDataSource,
|
|
757
|
+
type ServerSelectionRule,
|
|
757
758
|
type ServerRequest,
|
|
758
759
|
type ServerResult,
|
|
759
760
|
type ServerController,
|
|
761
|
+
type ServerControllerOptions,
|
|
760
762
|
type ServerState,
|
|
761
763
|
type ServerSortModel,
|
|
762
764
|
type ServerFilterModel,
|
|
@@ -766,19 +768,26 @@ export {
|
|
|
766
768
|
type ServerMoreRow,
|
|
767
769
|
type ServerFooterRow,
|
|
768
770
|
type ServerSkeletonRow,
|
|
771
|
+
type ServerGrandTotalRow,
|
|
772
|
+
type ServerPlaceholderRow,
|
|
769
773
|
type ServerDisplayRow,
|
|
770
774
|
} from './server-data-source'
|
|
771
775
|
export {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
type
|
|
776
|
-
type
|
|
777
|
-
type
|
|
778
|
-
type
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
export {
|
|
776
|
+
createBlockCache,
|
|
777
|
+
createRowPlaceholder,
|
|
778
|
+
rowPlaceholderState,
|
|
779
|
+
type BlockCache,
|
|
780
|
+
type BlockCacheOptions,
|
|
781
|
+
type BlockCacheState,
|
|
782
|
+
type BlockFetchResult,
|
|
783
|
+
type BlockState,
|
|
784
|
+
} from './server-block-cache'
|
|
785
|
+
export {
|
|
786
|
+
toServerFilterColumns,
|
|
787
|
+
type GridRowModel,
|
|
788
|
+
type GridFilterState,
|
|
789
|
+
type GridRowModelSort,
|
|
790
|
+
} from './row-model'
|
|
782
791
|
export {
|
|
783
792
|
createNamedViews,
|
|
784
793
|
memoryViews,
|
package/src/row-model.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GridRowModel` - one prop instead of eleven.
|
|
3
|
+
*
|
|
4
|
+
* A grid backed by a server needs the same wiring every time: hand it the
|
|
5
|
+
* rows, tell it not to sort or filter locally, forward the sort and the filter
|
|
6
|
+
* to the backend, report which rows are on screen, say which rows have not
|
|
7
|
+
* loaded, and delegate selection. Written out by hand that is a dozen props
|
|
8
|
+
* and a filter-shape conversion that every app gets slightly differently:
|
|
9
|
+
*
|
|
10
|
+
* ```svelte
|
|
11
|
+
* <SvGrid data={s.rows} externalSort externalFilter loading={s.loading}
|
|
12
|
+
* onSortingChange={(x) => ctl.setSort(x)}
|
|
13
|
+
* onFiltersChange={(f) => ctl.setFilter({ global: f.global,
|
|
14
|
+
* columns: Object.fromEntries(f.columns.map((c) => [c.id, { ... }])) })}
|
|
15
|
+
* onVisibleRangeChange={(r) => ctl.setViewport(r.startIndex, r.endIndex)}
|
|
16
|
+
* ... />
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* An object that implements this interface carries all of it, so the same grid
|
|
20
|
+
* becomes:
|
|
21
|
+
*
|
|
22
|
+
* ```svelte
|
|
23
|
+
* <SvGrid rowModel={ctl} {columns} />
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* `createServerDataSource` returns one, and so does the Enterprise server row
|
|
27
|
+
* model - which is the point of the interface rather than a concrete type: the
|
|
28
|
+
* grid depends on the SHAPE, never on either implementation, and an app with
|
|
29
|
+
* its own fetching can satisfy it too.
|
|
30
|
+
*
|
|
31
|
+
* Every member except `subscribe` / `getRows` / `isLoading` is optional, and a
|
|
32
|
+
* prop written explicitly on `<SvGrid>` always wins over the model's answer -
|
|
33
|
+
* so adopting this is never all-or-nothing.
|
|
34
|
+
*/
|
|
35
|
+
import type { FilterOperator } from './SvGrid.types'
|
|
36
|
+
|
|
37
|
+
/** The filter state as the grid reports it, straight from `onFiltersChange`. */
|
|
38
|
+
export type GridFilterState = {
|
|
39
|
+
global: string
|
|
40
|
+
columns: Array<{
|
|
41
|
+
id: string
|
|
42
|
+
operator: FilterOperator
|
|
43
|
+
value: string
|
|
44
|
+
valueTo?: string
|
|
45
|
+
selectedValues?: Array<string>
|
|
46
|
+
}>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The sort the grid hands a row model: column id and direction, in priority order. */
|
|
50
|
+
export type GridRowModelSort = Array<{ id: string; desc: boolean }>
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What `<SvGrid rowModel>` consumes: rows, a loading flag, a change
|
|
54
|
+
* subscription, and whichever optional parts the model implements - sort,
|
|
55
|
+
* filter, viewport, placeholders, group accessors, selection, filter
|
|
56
|
+
* values, paging, pinned rows and pivot columns. `createServerDataSource`
|
|
57
|
+
* and the Enterprise `createServerRowModel` both satisfy it.
|
|
58
|
+
*/
|
|
59
|
+
export type GridRowModel<TData> = {
|
|
60
|
+
/**
|
|
61
|
+
* Register a callback for "something changed, re-read me". Returns an
|
|
62
|
+
* unsubscribe function, which the grid calls on unmount.
|
|
63
|
+
*/
|
|
64
|
+
subscribe(onChange: () => void): () => void
|
|
65
|
+
/** The rows to render, including any placeholders for unloaded ones. */
|
|
66
|
+
getRows(): ReadonlyArray<TData>
|
|
67
|
+
/** True while a fetch is in flight, for the loading overlay. */
|
|
68
|
+
isLoading(): boolean
|
|
69
|
+
/** Stable row id. Required once selection or editing is involved. */
|
|
70
|
+
getRowId?(row: TData, index: number): string
|
|
71
|
+
/** Sort on the server. Present means "do not sort locally". */
|
|
72
|
+
setSort?(sort: GridRowModelSort): void
|
|
73
|
+
/** Filter on the server. Present means "do not filter locally". */
|
|
74
|
+
setFilter?(filters: GridFilterState): void
|
|
75
|
+
/** Which rows are on screen, so a block-loading model knows what to fetch. */
|
|
76
|
+
setViewport?(startIndex: number, endIndex: number): void
|
|
77
|
+
/** Why a row has no data yet, or null when it does. */
|
|
78
|
+
rowPlaceholder?(row: TData, rowIndex: number): 'loading' | 'failed' | null
|
|
79
|
+
/** Retry the failed block a placeholder row belongs to. */
|
|
80
|
+
retryRow?(row: TData, rowIndex: number): void
|
|
81
|
+
/** Group / tree accessors, for the grid's treegrid keyboard and ARIA. */
|
|
82
|
+
group?: {
|
|
83
|
+
isGroup: (row: TData) => boolean
|
|
84
|
+
level: (row: TData) => number
|
|
85
|
+
expanded?: (row: TData) => boolean
|
|
86
|
+
onToggle: (row: TData) => void
|
|
87
|
+
}
|
|
88
|
+
/** Selection the grid does not own - see the `rowSelectionModel` prop. */
|
|
89
|
+
selection?: {
|
|
90
|
+
isSelected: (rowId: string, row: TData) => boolean
|
|
91
|
+
headerState: () => 'none' | 'some' | 'all'
|
|
92
|
+
toggle: (rowId: string, row: TData, next: boolean) => void
|
|
93
|
+
toggleAll: (next: boolean) => void
|
|
94
|
+
selectedCount?: () => number | null
|
|
95
|
+
bulkUpdate?: (patch: Record<string, unknown>) => Promise<number>
|
|
96
|
+
}
|
|
97
|
+
/** Distinct values for a column's set filter, fetched from the server. */
|
|
98
|
+
filterValues?(columnId: string): Promise<Array<string>>
|
|
99
|
+
/** Server-side paging. Omit it for infinite scrolling. */
|
|
100
|
+
pagination?: {
|
|
101
|
+
pageIndex: number
|
|
102
|
+
pageSize: number
|
|
103
|
+
rowCount: number
|
|
104
|
+
setPage(pageIndex: number): void
|
|
105
|
+
setPageSize(pageSize: number): void
|
|
106
|
+
/** Choices for the footer's page-size selector. */
|
|
107
|
+
pageSizes?: number[]
|
|
108
|
+
/**
|
|
109
|
+
* Ask the grid for as many rows as its body fits: it measures and calls
|
|
110
|
+
* `setPageSize` whenever the fit changes.
|
|
111
|
+
*/
|
|
112
|
+
autoPageSize?: boolean
|
|
113
|
+
}
|
|
114
|
+
/** Rows to pin above / below the scrolling body - a grand total, say. */
|
|
115
|
+
readonly pinnedTopRows?: ReadonlyArray<TData>
|
|
116
|
+
readonly pinnedBottomRows?: ReadonlyArray<TData>
|
|
117
|
+
/**
|
|
118
|
+
* Columns that replace the grid's `columns` while the model is in pivot
|
|
119
|
+
* mode, or null. Read after every change notification.
|
|
120
|
+
*/
|
|
121
|
+
readonly pivotResultColumns?: ReadonlyArray<unknown> | null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Turn the grid's filter payload into the column-keyed map a `ServerRequest`
|
|
126
|
+
* carries. Exported because a hand-written `GridRowModel` needs exactly this
|
|
127
|
+
* conversion, and copying it is how the two shapes drift apart.
|
|
128
|
+
*/
|
|
129
|
+
export function toServerFilterColumns(
|
|
130
|
+
filters: GridFilterState,
|
|
131
|
+
): Record<
|
|
132
|
+
string,
|
|
133
|
+
{ operator: string; value: string; valueTo?: string; selectedValues?: Array<string> }
|
|
134
|
+
> {
|
|
135
|
+
return Object.fromEntries(
|
|
136
|
+
filters.columns.map((c) => [
|
|
137
|
+
c.id,
|
|
138
|
+
{
|
|
139
|
+
operator: c.operator,
|
|
140
|
+
value: c.value,
|
|
141
|
+
...(c.valueTo !== undefined ? { valueTo: c.valueTo } : {}),
|
|
142
|
+
...(c.selectedValues !== undefined ? { selectedValues: c.selectedValues } : {}),
|
|
143
|
+
},
|
|
144
|
+
]),
|
|
145
|
+
)
|
|
146
|
+
}
|
package/src/selection.test.ts
CHANGED
|
@@ -39,6 +39,9 @@ function makeCtx(overrides: any = {}) {
|
|
|
39
39
|
allRows: [],
|
|
40
40
|
allColumns: [],
|
|
41
41
|
headerSelectionState: 'none',
|
|
42
|
+
// The real controller always supplies this; a fake that omits it would
|
|
43
|
+
// make every select-all path throw rather than assert.
|
|
44
|
+
placeholderStateOf: () => null,
|
|
42
45
|
userHasActivatedCell: false,
|
|
43
46
|
enableCellSelectionEffective: true,
|
|
44
47
|
selectionRange: { anchor: null, focus: null },
|
package/src/selection.ts
CHANGED
|
@@ -21,16 +21,45 @@ export function createSelection<
|
|
|
21
21
|
TData extends RowData = RowData,
|
|
22
22
|
>(ctx: any) {
|
|
23
23
|
function isRowSelected(rowId: string) {
|
|
24
|
+
const model = ctx.props.rowSelectionModel;
|
|
25
|
+
if (model) {
|
|
26
|
+
const row = ctx.allRows.find((r: any) => r.id === rowId);
|
|
27
|
+
return row ? model.isSelected(rowId, row.original) : false;
|
|
28
|
+
}
|
|
24
29
|
return Boolean(ctx.rowSelectionState[rowId]);
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
function toggleRowSelectionById(rowId: string) {
|
|
33
|
+
const model = ctx.props.rowSelectionModel;
|
|
34
|
+
if (model) {
|
|
35
|
+
const row = ctx.allRows.find((r: any) => r.id === rowId);
|
|
36
|
+
if (!row) return;
|
|
37
|
+
model.toggle(rowId, row.original, !model.isSelected(rowId, row.original));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
28
40
|
ctx.grid.setRowSelection((prev: any) => ({ ...prev, [rowId]: !prev[rowId] }));
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
function toggleSelectAllRows() {
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
setSelectAllRows(ctx.headerSelectionState !== "all");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Select or clear every row, for the header checkbox and for
|
|
49
|
+
* `api.selectAllRows()`.
|
|
50
|
+
*/
|
|
51
|
+
function setSelectAllRows(select: boolean) {
|
|
52
|
+
// An external model decides what "all" means - possibly a million rows
|
|
53
|
+
// the grid has never seen - so it gets the intent, not a row list.
|
|
54
|
+
if (ctx.props.rowSelectionModel) {
|
|
55
|
+
ctx.props.rowSelectionModel.toggleAll(select);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Placeholder rows are excluded: there is no row there to select yet,
|
|
59
|
+
// and ticking one would leave a selected id that never resolves.
|
|
60
|
+
const selectable = ctx.allRows.filter(
|
|
61
|
+
(row: any) => !isGroupRow(row) && !ctx.placeholderStateOf(row),
|
|
62
|
+
);
|
|
34
63
|
ctx.grid.setRowSelection((prev: any) => {
|
|
35
64
|
const next = { ...prev };
|
|
36
65
|
for (const row of selectable) {
|
|
@@ -687,6 +716,7 @@ export function createSelection<
|
|
|
687
716
|
isRowSelected,
|
|
688
717
|
toggleRowSelectionById,
|
|
689
718
|
toggleSelectAllRows,
|
|
719
|
+
setSelectAllRows,
|
|
690
720
|
setActiveCell,
|
|
691
721
|
scrollActiveCellIntoView,
|
|
692
722
|
setSelection,
|