@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.
Files changed (71) hide show
  1. package/README.md +3 -3
  2. package/dist/SvGrid.controller.svelte.d.ts +12 -2
  3. package/dist/SvGrid.controller.svelte.js +191 -8
  4. package/dist/SvGrid.css +73 -0
  5. package/dist/SvGrid.svelte +82 -3
  6. package/dist/SvGrid.types.d.ts +105 -3
  7. package/dist/build-api.js +11 -6
  8. package/dist/cdn/{GridMenus-7kbpnnBW.js → GridMenus-CLI93jke.js} +26 -26
  9. package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-CwooL3QA.js} +26 -26
  10. package/dist/cdn/server-block-cache-CGoWz-87.js +237 -0
  11. package/dist/cdn/{src-BKhZ6eXd.js → src-B6HXdfHY.js} +4099 -4140
  12. package/dist/cdn/{src-V1uu8iE9.js → src-C6Iowdvs.js} +4052 -4093
  13. package/dist/cdn/svgrid.js +27 -26
  14. package/dist/cdn/svgrid.svelte-external.js +27 -26
  15. package/dist/cdn/validate-CchXzyrX.js +76 -0
  16. package/dist/core.js +36 -13
  17. package/dist/editing.js +10 -0
  18. package/dist/grid-icons.d.ts +2 -2
  19. package/dist/grid-icons.js +2 -2
  20. package/dist/grid-messages.d.ts +2 -0
  21. package/dist/grid-messages.js +2 -0
  22. package/dist/index.d.ts +3 -4
  23. package/dist/index.js +2 -3
  24. package/dist/row-model.d.ts +133 -0
  25. package/dist/row-model.js +16 -0
  26. package/dist/selection.d.ts +2 -1
  27. package/dist/selection.js +30 -2
  28. package/dist/server-block-cache.d.ts +214 -0
  29. package/dist/server-block-cache.js +531 -0
  30. package/dist/server-data-source.d.ts +240 -6
  31. package/dist/server-data-source.js +220 -20
  32. package/dist/server.d.ts +13 -0
  33. package/dist/server.js +13 -0
  34. package/dist/validate.d.ts +2 -0
  35. package/dist/validate.js +8 -3
  36. package/package.json +6 -1
  37. package/src/SvGrid.controller.svelte.ts +190 -8
  38. package/src/SvGrid.css +73 -0
  39. package/src/SvGrid.svelte +82 -3
  40. package/src/SvGrid.types.ts +105 -3
  41. package/src/build-api.ts +11 -4
  42. package/src/core.rowmodel-cache.test.ts +51 -0
  43. package/src/core.ts +33 -12
  44. package/src/editing.ts +8 -0
  45. package/src/grid-icons.ts +2 -2
  46. package/src/grid-messages.ts +5 -0
  47. package/src/icon-seam.test.ts +2 -4
  48. package/src/index.ts +19 -10
  49. package/src/row-model.ts +146 -0
  50. package/src/selection.test.ts +3 -0
  51. package/src/selection.ts +32 -2
  52. package/src/server-block-cache.test.ts +645 -0
  53. package/src/server-block-cache.ts +677 -0
  54. package/src/server-data-source.infinite.test.ts +343 -0
  55. package/src/server-data-source.ts +450 -27
  56. package/src/server.ts +47 -0
  57. package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
  58. package/src/svgrid.row-model-seam.svelte.test.ts +289 -0
  59. package/src/validate.test.ts +10 -0
  60. package/src/validate.ts +10 -3
  61. package/dist/SvGroupCell.svelte +0 -141
  62. package/dist/SvGroupCell.svelte.d.ts +0 -49
  63. package/dist/SvRowGroupPanel.svelte +0 -186
  64. package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
  65. package/dist/cdn/validate-_CDJzgIo.js +0 -75
  66. package/dist/server-group-model.d.ts +0 -98
  67. package/dist/server-group-model.js +0 -263
  68. package/src/SvGroupCell.svelte +0 -141
  69. package/src/SvRowGroupPanel.svelte +0 -186
  70. package/src/server-group-model.test.ts +0 -294
  71. package/src/server-group-model.ts +0 -370
@@ -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
- data: ReadonlyArray<TData>;
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 `serverGroupRows`
2123
- * + `SvGroupCell` for the visual expander. Every accessor receives the row data.
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
- const next: Record<string, boolean> = {};
739
- for (const row of ctx.allRows) if (!isGroupRow(row)) next[row.id] = true;
740
- ctx.grid.setRowSelection(() => next);
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
- const total = ctx.allRowsBeforePagination.length;
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
- const columnIndexById = new Map<string, number>()
1762
- for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)
1763
- const columnCount = columns.length
1764
-
1765
- // One shared context for every row in this table, so a row carries a
1766
- // pointer rather than a closure scope. See BASE_ROW_METHODS.
1767
- const rowCtx: BaseRowCtx<TData> = {
1768
- grid: grid as SvGrid<TData>,
1769
- store,
1770
- columns,
1771
- columnCount,
1772
- columnIndexById,
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 standalone `SvRowGroupPanel` render
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.
@@ -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:
@@ -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', 'SvRowGroupPanel.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), the `ic` snippet in SvRowGroupPanel, and
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
- createServerGroupModel,
773
- serverGroupRows,
774
- serverGroupNav,
775
- type ServerGroupController,
776
- type ServerGroupControllerOptions,
777
- type ServerGroupState,
778
- type ServerGroupGridRow,
779
- } from './server-group-model'
780
- export { default as SvGroupCell } from './SvGroupCell.svelte'
781
- export { default as SvRowGroupPanel } from './SvRowGroupPanel.svelte'
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,
@@ -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
+ }
@@ -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
- const selectable = ctx.allRows.filter((row: any) => !isGroupRow(row));
33
- const select = ctx.headerSelectionState !== "all";
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,