@svgrid/grid 3.0.3 → 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 (318) hide show
  1. package/README.md +5 -4
  2. package/dist/GridMenus.svelte +44 -2
  3. package/dist/SvChartMenu.svelte +69 -0
  4. package/dist/SvChartMenu.svelte.d.ts +11 -0
  5. package/dist/SvChartPanes.svelte +113 -0
  6. package/dist/SvChartPanes.svelte.d.ts +20 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +122 -41
  8. package/dist/SvGrid.controller.svelte.js +845 -207
  9. package/dist/SvGrid.css +178 -2
  10. package/dist/SvGrid.svelte +467 -516
  11. package/dist/SvGrid.types.d.ts +315 -12
  12. package/dist/SvGridCellEditor.svelte +17 -0
  13. package/dist/SvGridChart.svelte +2257 -329
  14. package/dist/SvGridChart.svelte.d.ts +6 -106
  15. package/dist/SvGridChart.types.d.ts +338 -0
  16. package/dist/SvGridChart.types.js +1 -0
  17. package/dist/SvGridChartBuilder.svelte +323 -0
  18. package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
  19. package/dist/SvGridChartPanel.svelte +221 -275
  20. package/dist/SvGridChartPickers.svelte +417 -0
  21. package/dist/SvGridChartPickers.svelte.d.ts +70 -0
  22. package/dist/SvGridChartView.svelte +13 -14
  23. package/dist/SvModal.svelte +7 -1
  24. package/dist/SvModal.svelte.d.ts +5 -0
  25. package/dist/ai.d.ts +40 -8
  26. package/dist/ai.js +135 -24
  27. package/dist/aria.d.ts +11 -0
  28. package/dist/build-api.js +119 -28
  29. package/dist/cdn/GridMenus-CLI93jke.js +635 -0
  30. package/dist/cdn/{GridMenus-DhdPF700.js → GridMenus-CwooL3QA.js} +240 -207
  31. package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
  32. package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
  33. package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
  34. package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
  35. package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
  36. package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
  37. package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
  38. package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
  39. package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
  40. package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
  41. package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
  42. package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
  43. package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
  44. package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
  45. package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
  46. package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
  47. package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
  48. package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
  49. package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
  50. package/dist/cdn/SvModal-CIZWCcad.js +397 -0
  51. package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
  52. package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
  53. package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
  54. package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
  55. package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
  56. package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
  57. package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
  58. package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
  59. package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
  60. package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
  61. package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
  62. package/dist/cdn/dismissable-DAHetSNk.js +44 -0
  63. package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
  64. package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
  65. package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
  66. package/dist/cdn/server-block-cache-CGoWz-87.js +237 -0
  67. package/dist/cdn/{src-D6XvA1Ij.js → src-B6HXdfHY.js} +10448 -10049
  68. package/dist/cdn/{src-Bugf5XlI.js → src-C6Iowdvs.js} +6908 -6509
  69. package/dist/cdn/svgrid.js +27 -15
  70. package/dist/cdn/svgrid.svelte-external.js +27 -15
  71. package/dist/cdn/validate-CchXzyrX.js +76 -0
  72. package/dist/chart-axes.d.ts +53 -0
  73. package/dist/chart-axes.js +351 -0
  74. package/dist/chart-cartesian.d.ts +88 -0
  75. package/dist/chart-cartesian.js +1862 -0
  76. package/dist/chart-decimate.d.ts +51 -0
  77. package/dist/chart-decimate.js +199 -0
  78. package/dist/chart-export-pdf.d.ts +47 -0
  79. package/dist/chart-export-pdf.js +187 -0
  80. package/dist/chart-export.d.ts +2 -1
  81. package/dist/chart-export.js +18 -3
  82. package/dist/chart-financial.d.ts +91 -0
  83. package/dist/chart-financial.js +175 -0
  84. package/dist/chart-flow.d.ts +10 -0
  85. package/dist/chart-flow.js +298 -0
  86. package/dist/chart-format.d.ts +18 -0
  87. package/dist/chart-format.js +65 -0
  88. package/dist/chart-grid.d.ts +8 -0
  89. package/dist/chart-grid.js +190 -0
  90. package/dist/chart-hierarchy.d.ts +17 -0
  91. package/dist/chart-hierarchy.js +146 -0
  92. package/dist/chart-indicators.d.ts +115 -0
  93. package/dist/chart-indicators.js +389 -0
  94. package/dist/chart-messages.d.ts +95 -0
  95. package/dist/chart-messages.js +92 -0
  96. package/dist/chart-motion.d.ts +16 -0
  97. package/dist/chart-motion.js +141 -0
  98. package/dist/chart-panel-messages.d.ts +265 -0
  99. package/dist/chart-panel-messages.js +285 -0
  100. package/dist/chart-pivot.d.ts +87 -0
  101. package/dist/chart-pivot.js +156 -0
  102. package/dist/chart-polar.d.ts +25 -0
  103. package/dist/chart-polar.js +700 -0
  104. package/dist/chart-samples.d.ts +16 -0
  105. package/dist/chart-samples.js +153 -0
  106. package/dist/chart-scale.d.ts +113 -0
  107. package/dist/chart-scale.js +405 -0
  108. package/dist/chart-stats.d.ts +111 -0
  109. package/dist/chart-stats.js +419 -0
  110. package/dist/chart-stream.d.ts +45 -0
  111. package/dist/chart-stream.js +91 -0
  112. package/dist/chart-summary.d.ts +27 -0
  113. package/dist/chart-summary.js +205 -0
  114. package/dist/chart-sync.svelte.d.ts +32 -0
  115. package/dist/chart-sync.svelte.js +23 -0
  116. package/dist/chart-table.d.ts +35 -0
  117. package/dist/chart-table.js +165 -0
  118. package/dist/chart-trend.d.ts +19 -0
  119. package/dist/chart-trend.js +61 -0
  120. package/dist/chart-types.d.ts +1456 -0
  121. package/dist/chart-types.js +6 -0
  122. package/dist/chart-validate.d.ts +43 -0
  123. package/dist/chart-validate.js +272 -0
  124. package/dist/chart-zoom.d.ts +40 -0
  125. package/dist/chart-zoom.js +145 -0
  126. package/dist/chart.d.ts +186 -865
  127. package/dist/chart.js +460 -2204
  128. package/dist/clipboard.d.ts +1 -0
  129. package/dist/clipboard.js +137 -18
  130. package/dist/column-resize.d.ts +3 -0
  131. package/dist/column-resize.js +4 -1
  132. package/dist/columns.js +3 -0
  133. package/dist/command-context.d.ts +19 -0
  134. package/dist/command-context.js +116 -0
  135. package/dist/core.d.ts +7 -0
  136. package/dist/core.js +36 -13
  137. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  138. package/dist/editing.d.ts +2 -8
  139. package/dist/editing.js +272 -33
  140. package/dist/fill-patterns.d.ts +0 -5
  141. package/dist/fill-patterns.js +56 -2
  142. package/dist/grid-icons.d.ts +2 -2
  143. package/dist/grid-icons.js +2 -2
  144. package/dist/grid-messages.d.ts +12 -3
  145. package/dist/grid-messages.js +7 -0
  146. package/dist/history.d.ts +83 -0
  147. package/dist/history.js +133 -0
  148. package/dist/index.d.ts +27 -10
  149. package/dist/index.js +22 -5
  150. package/dist/keyboard-handlers.js +88 -31
  151. package/dist/keyboard.d.ts +55 -0
  152. package/dist/keyboard.js +132 -0
  153. package/dist/menus.d.ts +3 -1
  154. package/dist/menus.js +7 -2
  155. package/dist/merges.d.ts +97 -0
  156. package/dist/merges.js +147 -0
  157. package/dist/row-model.d.ts +133 -0
  158. package/dist/row-model.js +16 -0
  159. package/dist/row-resize.d.ts +3 -0
  160. package/dist/row-resize.js +14 -0
  161. package/dist/scroll-sync.d.ts +1 -0
  162. package/dist/scroll-sync.js +24 -0
  163. package/dist/selection.d.ts +18 -1
  164. package/dist/selection.js +138 -18
  165. package/dist/server-block-cache.d.ts +214 -0
  166. package/dist/server-block-cache.js +531 -0
  167. package/dist/server-data-source.d.ts +240 -6
  168. package/dist/server-data-source.js +220 -20
  169. package/dist/server.d.ts +13 -0
  170. package/dist/server.js +13 -0
  171. package/dist/shortcut-registry.d.ts +145 -0
  172. package/dist/shortcut-registry.js +44 -0
  173. package/dist/svgrid-wrapper.types.d.ts +213 -22
  174. package/dist/validate.d.ts +2 -0
  175. package/dist/validate.js +8 -3
  176. package/dist/virtualization/virtualizer.js +38 -17
  177. package/package.json +11 -1
  178. package/src/GridMenus.svelte +44 -2
  179. package/src/SvChartMenu.svelte +69 -0
  180. package/src/SvChartPanes.svelte +113 -0
  181. package/src/SvChartPanes.test.ts +66 -0
  182. package/src/SvGrid.controller.svelte.ts +818 -179
  183. package/src/SvGrid.css +178 -2
  184. package/src/SvGrid.svelte +467 -516
  185. package/src/SvGrid.types.ts +293 -11
  186. package/src/SvGridCellEditor.svelte +17 -0
  187. package/src/SvGridChart.svelte +2257 -329
  188. package/src/SvGridChart.test.ts +1419 -1
  189. package/src/SvGridChart.types.ts +336 -0
  190. package/src/SvGridChartBuilder.svelte +323 -0
  191. package/src/SvGridChartPanel.svelte +221 -275
  192. package/src/SvGridChartPickers.svelte +417 -0
  193. package/src/SvGridChartView.svelte +13 -14
  194. package/src/SvModal.svelte +7 -1
  195. package/src/SvModal.test.ts +9 -0
  196. package/src/ai.test.ts +57 -2
  197. package/src/ai.ts +166 -22
  198. package/src/aria.d.ts +11 -0
  199. package/src/build-api.coverage.test.ts +184 -0
  200. package/src/build-api.ts +118 -22
  201. package/src/chart-axes.test.ts +573 -0
  202. package/src/chart-axes.ts +342 -0
  203. package/src/chart-cartesian.ts +1863 -0
  204. package/src/chart-decimate.test.ts +249 -0
  205. package/src/chart-decimate.ts +186 -0
  206. package/src/chart-export-pdf.test.ts +91 -0
  207. package/src/chart-export-pdf.ts +207 -0
  208. package/src/chart-export.test.ts +14 -0
  209. package/src/chart-export.ts +13 -3
  210. package/src/chart-financial.ts +208 -0
  211. package/src/chart-flow.ts +286 -0
  212. package/src/chart-format.ts +67 -0
  213. package/src/chart-grid.ts +179 -0
  214. package/src/chart-hierarchy.ts +135 -0
  215. package/src/chart-indicators.test.ts +443 -0
  216. package/src/chart-indicators.ts +384 -0
  217. package/src/chart-messages.test.ts +21 -0
  218. package/src/chart-messages.ts +184 -0
  219. package/src/chart-motion.test.ts +71 -0
  220. package/src/chart-motion.ts +148 -0
  221. package/src/chart-panel-messages.test.ts +29 -0
  222. package/src/chart-panel-messages.ts +550 -0
  223. package/src/chart-pivot.test.ts +54 -0
  224. package/src/chart-pivot.ts +225 -0
  225. package/src/chart-polar.ts +674 -0
  226. package/src/chart-samples.ts +106 -0
  227. package/src/chart-scale.ts +394 -0
  228. package/src/chart-series-types.test.ts +714 -0
  229. package/src/chart-stats.ts +409 -0
  230. package/src/chart-stream.test.ts +75 -0
  231. package/src/chart-stream.ts +114 -0
  232. package/src/chart-summary.test.ts +90 -0
  233. package/src/chart-summary.ts +197 -0
  234. package/src/chart-sync.svelte.ts +41 -0
  235. package/src/chart-table.test.ts +76 -0
  236. package/src/chart-table.ts +178 -0
  237. package/src/chart-trend.ts +47 -0
  238. package/src/chart-types.ts +1309 -0
  239. package/src/chart-validate.test.ts +100 -0
  240. package/src/chart-validate.ts +247 -0
  241. package/src/chart-zoom.test.ts +137 -0
  242. package/src/chart-zoom.ts +124 -0
  243. package/src/chart.coverage.test.ts +81 -0
  244. package/src/chart.test.ts +56 -3
  245. package/src/chart.ts +557 -2784
  246. package/src/clipboard.test.ts +151 -0
  247. package/src/clipboard.ts +145 -16
  248. package/src/column-resize.ts +7 -1
  249. package/src/columns.ts +2 -0
  250. package/src/command-context.test.ts +200 -0
  251. package/src/command-context.ts +137 -0
  252. package/src/core.rowmodel-cache.test.ts +51 -0
  253. package/src/core.ts +40 -12
  254. package/src/editing.test.ts +281 -3
  255. package/src/editing.ts +264 -38
  256. package/src/fill-patterns.test.ts +35 -0
  257. package/src/fill-patterns.ts +56 -2
  258. package/src/grid-icons.ts +2 -2
  259. package/src/grid-messages.ts +21 -3
  260. package/src/history.test.ts +196 -0
  261. package/src/history.ts +162 -0
  262. package/src/icon-seam.test.ts +5 -6
  263. package/src/index.ts +169 -12
  264. package/src/keyboard-handlers.coverage.test.ts +126 -6
  265. package/src/keyboard-handlers.ts +91 -30
  266. package/src/keyboard-shortcuts.seam.test.ts +200 -0
  267. package/src/keyboard.test.ts +124 -1
  268. package/src/keyboard.ts +142 -0
  269. package/src/menus.test.ts +26 -0
  270. package/src/menus.ts +10 -3
  271. package/src/merges.test.ts +112 -0
  272. package/src/merges.ts +168 -0
  273. package/src/pivot.grid.test.ts +40 -5
  274. package/src/row-model.ts +146 -0
  275. package/src/row-resize.test.ts +44 -0
  276. package/src/row-resize.ts +17 -0
  277. package/src/scroll-sync.test.ts +42 -1
  278. package/src/scroll-sync.ts +25 -0
  279. package/src/selection.multi-range.test.ts +34 -0
  280. package/src/selection.test.ts +33 -2
  281. package/src/selection.ts +143 -21
  282. package/src/server-block-cache.test.ts +645 -0
  283. package/src/server-block-cache.ts +677 -0
  284. package/src/server-data-source.infinite.test.ts +343 -0
  285. package/src/server-data-source.ts +450 -27
  286. package/src/server.ts +47 -0
  287. package/src/shortcut-registry.test.ts +97 -0
  288. package/src/shortcut-registry.ts +181 -0
  289. package/src/svgrid-wrapper.types.ts +184 -13
  290. package/src/svgrid.behavior.test.ts +135 -0
  291. package/src/svgrid.charting.test.ts +513 -3
  292. package/src/svgrid.context-menu.test.ts +31 -0
  293. package/src/svgrid.new-features.wrapper.test.ts +10 -3
  294. package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
  295. package/src/svgrid.row-model-seam.svelte.test.ts +289 -0
  296. package/src/validate.test.ts +10 -0
  297. package/src/validate.ts +10 -3
  298. package/src/virtualization/virtualizer.ts +39 -17
  299. package/dist/SvGroupCell.svelte +0 -141
  300. package/dist/SvGroupCell.svelte.d.ts +0 -49
  301. package/dist/SvRowGroupPanel.svelte +0 -186
  302. package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
  303. package/dist/cdn/GridMenus-CHOMnW-c.js +0 -602
  304. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  305. package/dist/cdn/SvGridChart-1PMSp2LZ.js +0 -1481
  306. package/dist/cdn/SvGridChart-D02avhyr.js +0 -1480
  307. package/dist/cdn/SvGridChartPanel-DJnViOv4.js +0 -834
  308. package/dist/cdn/SvGridChartPanel-vPTNerFZ.js +0 -810
  309. package/dist/cdn/SvGridChartView-C--HhdUo.js +0 -56
  310. package/dist/cdn/SvGridChartView-KyyFG-wY.js +0 -55
  311. package/dist/cdn/chart-DLg3_zTQ.js +0 -1652
  312. package/dist/cdn/validate-_CDJzgIo.js +0 -75
  313. package/dist/server-group-model.d.ts +0 -98
  314. package/dist/server-group-model.js +0 -263
  315. package/src/SvGroupCell.svelte +0 -141
  316. package/src/SvRowGroupPanel.svelte +0 -186
  317. package/src/server-group-model.test.ts +0 -294
  318. package/src/server-group-model.ts +0 -370
@@ -0,0 +1,352 @@
1
+ /**
2
+ * `<SvGrid rowModel={ctl} />` - the one-prop integration.
3
+ *
4
+ * The seams below it are tested in `svgrid.row-model-seam.svelte.test.ts`;
5
+ * this suite is about what the prop itself is for: that handing the grid a
6
+ * model wires all of those seams at once, that the model drives re-renders,
7
+ * and that writing a prop explicitly still wins - so adopting it is never
8
+ * all-or-nothing.
9
+ */
10
+ import { describe, expect, it, vi } from 'vitest'
11
+ import { mount, unmount } from 'svelte'
12
+ import SvGrid from './SvGrid.svelte'
13
+ import {
14
+ createCoreRowModel,
15
+ createServerDataSource,
16
+ rowSelectionFeature,
17
+ rowSortingFeature,
18
+ tableFeatures,
19
+ type ColumnDef,
20
+ type GridRowModel,
21
+ type ServerDataSource,
22
+ type SvGridApi,
23
+ } from './index'
24
+
25
+ type Row = { id: number; name: string }
26
+ const features = tableFeatures({ rowSortingFeature, rowSelectionFeature })
27
+ const cols: ColumnDef<typeof features, Row>[] = [{ field: 'name', header: 'Name', width: 160 }]
28
+
29
+ const tick = () => new Promise<void>((r) => setTimeout(r))
30
+ const settle = async () => {
31
+ for (let i = 0; i < 20; i += 1) await Promise.resolve()
32
+ await tick()
33
+ }
34
+
35
+ function mountGrid(props: Record<string, unknown>) {
36
+ return new Promise<{
37
+ api: SvGridApi<typeof features, Row>
38
+ target: HTMLElement
39
+ destroy: () => void
40
+ }>((res, rej) => {
41
+ const target = document.createElement('div')
42
+ document.body.appendChild(target)
43
+ const app = mount(SvGrid, {
44
+ target,
45
+ props: {
46
+ columns: cols,
47
+ features,
48
+ _rowModels: { coreRowModel: createCoreRowModel() },
49
+ containerHeight: 200,
50
+ virtualization: false,
51
+ onApiReady(api: SvGridApi<typeof features, Row>) {
52
+ res({ api, target, destroy: () => { unmount(app); target.remove() } })
53
+ },
54
+ ...props,
55
+ } as any,
56
+ })
57
+ queueMicrotask(() => {
58
+ if (!target.querySelector('[role="grid"]')) rej(new Error('no grid'))
59
+ })
60
+ })
61
+ }
62
+
63
+ /** A hand-written model, to prove the grid depends on the shape not the class. */
64
+ function handWritten(rows: Row[]) {
65
+ const listeners = new Set<() => void>()
66
+ let current = rows
67
+ let loading = false
68
+ const sorts: Array<unknown> = []
69
+ return {
70
+ sorts,
71
+ push(next: Row[]) {
72
+ current = next
73
+ for (const l of listeners) l()
74
+ },
75
+ setLoading(next: boolean) {
76
+ loading = next
77
+ for (const l of listeners) l()
78
+ },
79
+ model: {
80
+ subscribe(onChange: () => void) {
81
+ listeners.add(onChange)
82
+ return () => listeners.delete(onChange)
83
+ },
84
+ getRows: () => current,
85
+ isLoading: () => loading,
86
+ getRowId: (r: Row) => String(r.id),
87
+ setSort: (s: unknown) => sorts.push(s),
88
+ } satisfies GridRowModel<Row>,
89
+ }
90
+ }
91
+
92
+ describe('the rowModel prop', () => {
93
+ it('renders the rows the model reports, with no data prop at all', async () => {
94
+ const source = handWritten([
95
+ { id: 1, name: 'Ada' },
96
+ { id: 2, name: 'Grace' },
97
+ ])
98
+ const { target, destroy } = await mountGrid({ rowModel: source.model })
99
+ await tick()
100
+
101
+ expect(target.textContent).toContain('Ada')
102
+ expect(target.textContent).toContain('Grace')
103
+ destroy()
104
+ })
105
+
106
+ it('re-renders when the model says it changed', async () => {
107
+ const source = handWritten([{ id: 1, name: 'Ada' }])
108
+ const { target, destroy } = await mountGrid({ rowModel: source.model })
109
+ await tick()
110
+ expect(target.textContent).not.toContain('Linus')
111
+
112
+ source.push([{ id: 1, name: 'Ada' }, { id: 9, name: 'Linus' }])
113
+ await vi.waitFor(() => expect(target.textContent).toContain('Linus'))
114
+ destroy()
115
+ })
116
+
117
+ it('unsubscribes on unmount', async () => {
118
+ const source = handWritten([{ id: 1, name: 'Ada' }])
119
+ const { destroy } = await mountGrid({ rowModel: source.model })
120
+ await tick()
121
+ destroy()
122
+ await tick()
123
+
124
+ // A model that outlives the grid must not keep it alive through a
125
+ // listener; pushing after unmount should reach nobody and throw nothing.
126
+ expect(() => source.push([{ id: 2, name: 'Grace' }])).not.toThrow()
127
+ })
128
+
129
+ it('takes over sorting when the model sorts, and says so to the grid', async () => {
130
+ const source = handWritten([
131
+ { id: 1, name: 'Zoe' },
132
+ { id: 2, name: 'Ada' },
133
+ ])
134
+ const { api, target, destroy } = await mountGrid({ rowModel: source.model, sortable: true })
135
+ await tick()
136
+
137
+ api.setSort('name', 'asc')
138
+ await tick()
139
+
140
+ // The model was told; the grid did NOT reorder locally, because a model
141
+ // with `setSort` implies `externalSort`. (The grid also reports the
142
+ // empty initial sort on mount, as it does for any onSortingChange.)
143
+ expect(source.sorts.at(-1)).toEqual([{ id: 'name', desc: false }])
144
+ const firstRow = target.querySelector('tbody .sv-grid-row')!
145
+ expect(firstRow.textContent).toContain('Zoe')
146
+ destroy()
147
+ })
148
+
149
+ it('lets an explicit prop win over the model', async () => {
150
+ const source = handWritten([{ id: 1, name: 'Ada' }])
151
+ const { target, destroy } = await mountGrid({
152
+ rowModel: source.model,
153
+ data: [{ id: 7, name: 'Override' }],
154
+ })
155
+ await tick()
156
+
157
+ expect(target.textContent).toContain('Override')
158
+ expect(target.textContent).not.toContain('Ada')
159
+ destroy()
160
+ })
161
+
162
+ it('drives a real server controller end to end', async () => {
163
+ const source: ServerDataSource<Row> = {
164
+ async getRows(req) {
165
+ const all: Row[] = Array.from({ length: 250 }, (_, i) => ({ id: i, name: `Row ${i}` }))
166
+ const sorted = req.sortModel[0]?.desc ? [...all].reverse() : all
167
+ return { rows: sorted.slice(req.startRow, req.endRow), rowCount: all.length }
168
+ },
169
+ }
170
+ const ctl = createServerDataSource<Row>(source, {
171
+ mode: 'infinite',
172
+ blockSize: 100,
173
+ onChange: () => {},
174
+ })
175
+
176
+ const { target, destroy } = await mountGrid({ rowModel: ctl })
177
+
178
+ // The grid reported its visible range, the controller fetched block 0, and
179
+ // the rows arrived - with no wiring in this test beyond the one prop.
180
+ await vi.waitFor(() => expect(target.textContent).toContain('Row 0'))
181
+ expect(ctl.getCacheState().length).toBeGreaterThan(0)
182
+ expect(ctl.getRows()).toHaveLength(250)
183
+ destroy()
184
+ ctl.dispose()
185
+ }, 20_000)
186
+
187
+ it('wires the pager from a page-mode controller', async () => {
188
+ const source: ServerDataSource<Row> = {
189
+ async getRows(req) {
190
+ const all: Row[] = Array.from({ length: 95 }, (_, i) => ({ id: i, name: `Row ${i}` }))
191
+ return { rows: all.slice(req.startRow, req.endRow), rowCount: all.length }
192
+ },
193
+ }
194
+ const ctl = createServerDataSource<Row>(source, { pageSize: 20, onChange: () => {} })
195
+ ctl.refresh()
196
+ await settle()
197
+
198
+ const { api, destroy } = await mountGrid({ rowModel: ctl, pageable: true })
199
+
200
+ // 95 rows on the server, not the 20 in hand: the count came from the model.
201
+ await vi.waitFor(() => expect(api.getPageInfo().pageCount).toBe(5))
202
+ expect(api.getPageInfo().pageSize).toBe(20)
203
+ destroy()
204
+ ctl.dispose()
205
+ }, 20_000)
206
+
207
+ it('takes the page-size choices from the model and sizes the page to the body on request', async () => {
208
+ const calls: number[] = []
209
+ let pageSize = 10
210
+ const rows: Row[] = Array.from({ length: 50 }, (_, i) => ({ id: i, name: `Row ${i}` }))
211
+ const model: GridRowModel<Row> = {
212
+ subscribe: () => () => {},
213
+ getRows: () => rows.slice(0, pageSize),
214
+ isLoading: () => false,
215
+ get pagination() {
216
+ return {
217
+ pageIndex: 0,
218
+ pageSize,
219
+ rowCount: 50,
220
+ setPage: () => {},
221
+ setPageSize: (n: number) => {
222
+ calls.push(n)
223
+ pageSize = n
224
+ },
225
+ pageSizes: [7, 14, 21],
226
+ autoPageSize: true,
227
+ }
228
+ },
229
+ }
230
+ // jsdom lays nothing out; give every element a 100px box so the grid
231
+ // measures its body on mount (the way the responsive test fakes width).
232
+ const desc = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'clientHeight')
233
+ Object.defineProperty(HTMLElement.prototype, 'clientHeight', { configurable: true, get: () => 100 })
234
+ try {
235
+ const { api, target, destroy } = await mountGrid({ rowModel: model, pageable: true, rowHeight: 20 })
236
+ // The footer offers the model's sizes, not the grid's defaults.
237
+ await vi.waitFor(() => expect(target.querySelector('.sv-grid-pagination')).toBeTruthy())
238
+ expect(api.getOption('pageSizeOptions')).toEqual([7, 14, 21])
239
+ // Auto size: 100px of body over 20px rows is a page of five.
240
+ await vi.waitFor(() => expect(calls.at(-1)).toBe(5))
241
+ destroy()
242
+ } finally {
243
+ if (desc) Object.defineProperty(HTMLElement.prototype, 'clientHeight', desc)
244
+ else delete (HTMLElement.prototype as { clientHeight?: number }).clientHeight
245
+ }
246
+ }, 20_000)
247
+ })
248
+
249
+ describe('editing under a row model', () => {
250
+ type Node = Row & { kind: "group" | "leaf" }
251
+ it('refuses to edit a server group row or a placeholder, and edits a leaf', async () => {
252
+ const nodes: Node[] = [
253
+ { id: 1, name: 'EMEA', kind: 'group' },
254
+ { id: 2, name: 'Ada', kind: 'leaf' },
255
+ ]
256
+ const model: GridRowModel<Node> = {
257
+ subscribe: () => () => {},
258
+ getRows: () => nodes,
259
+ isLoading: () => false,
260
+ getRowId: (r) => String(r.id),
261
+ group: {
262
+ isGroup: (r) => r.kind === 'group',
263
+ level: () => 0,
264
+ expanded: () => false,
265
+ onToggle: () => {},
266
+ },
267
+ }
268
+ const { api, target, destroy } = await mountGrid({
269
+ rowModel: model,
270
+ columns: [{ field: 'name', header: 'Name', width: 160, editorType: 'text' }],
271
+ editable: true,
272
+ })
273
+ try {
274
+ // The group row: nothing opens, whatever the column says.
275
+ api.startEditing(0, 'name')
276
+ await tick()
277
+ expect(target.querySelector('.sv-grid-cell input')).toBeNull()
278
+ // The leaf: an editor.
279
+ api.startEditing(1, 'name')
280
+ await vi.waitFor(() => expect(target.querySelector('.sv-grid-cell input')).not.toBeNull())
281
+ } finally {
282
+ destroy()
283
+ }
284
+ })
285
+ })
286
+
287
+ describe('the rowModel prop with virtualization on', () => {
288
+ /** 100k rows, so only a window is rendered and most rows are placeholders. */
289
+ function bigSource(): ServerDataSource<Row> {
290
+ return {
291
+ async getRows(req) {
292
+ const rows: Row[] = []
293
+ for (let i = req.startRow; i < Math.min(req.endRow, 100_000); i += 1) {
294
+ rows.push({ id: i, name: `Row ${i}` })
295
+ }
296
+ return { rows, rowCount: 100_000 }
297
+ },
298
+ }
299
+ }
300
+
301
+ it('renders skeletons for the rows it has not loaded', async () => {
302
+ const ctl = createServerDataSource<Row>(bigSource(), {
303
+ mode: 'infinite',
304
+ blockSize: 100,
305
+ onChange: () => {},
306
+ })
307
+ const { target, destroy } = await mountGrid({
308
+ rowModel: ctl,
309
+ virtualization: true,
310
+ containerHeight: 300,
311
+ rowHeight: 30,
312
+ })
313
+
314
+ // Block 0 landed, so the window at the top is real data.
315
+ await vi.waitFor(() => expect(target.textContent).toContain('Row 0'))
316
+
317
+ // The grid claims the whole table, and everything outside the loaded
318
+ // block is a placeholder - which is what keeps the scrollbar honest.
319
+ const rows = ctl.getRows()
320
+ expect(rows).toHaveLength(100_000)
321
+ expect(ctl.rowPlaceholder?.(rows[50_000] as Row, 50_000)).toBe('loading')
322
+ destroy()
323
+ ctl.dispose()
324
+ }, 20_000)
325
+
326
+ it('draws skeleton cells for a row the model calls a placeholder', async () => {
327
+ // Straight through the seam, with no fetching involved: the question here
328
+ // is only whether `rowModel.rowPlaceholder` reaches the renderer. Driving
329
+ // it from a real controller cannot answer that - with virtualization off
330
+ // the grid reports every row as visible, so every block loads and no
331
+ // placeholder survives to be rendered.
332
+ const rows: Row[] = [
333
+ { id: 0, name: 'Ada' },
334
+ { id: 1, name: 'Grace' },
335
+ { id: 2, name: 'Linus' },
336
+ ]
337
+ const model: GridRowModel<Row> = {
338
+ subscribe: () => () => {},
339
+ getRows: () => rows,
340
+ isLoading: () => false,
341
+ rowPlaceholder: (row) => (row.id === 2 ? 'loading' : null),
342
+ }
343
+ const { target, destroy } = await mountGrid({ rowModel: model })
344
+ await tick()
345
+
346
+ const placeholder = target.querySelector('.sv-grid-placeholder-row')!
347
+ expect(placeholder).not.toBeNull()
348
+ expect(placeholder.querySelectorAll('.sv-grid-placeholder-skeleton').length).toBe(cols.length)
349
+ expect(target.textContent).toContain('Ada')
350
+ destroy()
351
+ })
352
+ })
@@ -0,0 +1,289 @@
1
+ /**
2
+ * The four seams a server-backed row model plugs into, all free:
3
+ *
4
+ * - `onVisibleRangeChange` - which rows are on screen, by index
5
+ * - `rowPlaceholder` / `onRetryRow` - rows whose data has not arrived
6
+ * - `rowSelectionModel` - selection the grid does not own
7
+ *
8
+ * They are deliberately generic: none of them mentions groups, blocks or a
9
+ * datasource, so the free infinite controller and the Enterprise row model
10
+ * plug into the same four holes, and an app with its own fetching can too.
11
+ */
12
+ import { describe, expect, it, vi } from 'vitest'
13
+ import { mount, unmount } from 'svelte'
14
+ import SvGrid from './SvGrid.svelte'
15
+ import {
16
+ createCoreRowModel,
17
+ rowSelectionFeature,
18
+ rowSortingFeature,
19
+ tableFeatures,
20
+ } from './index'
21
+ import type { ColumnDef, SvGridApi } from './index'
22
+
23
+ type Row = { id: number; name: string }
24
+ const features = tableFeatures({ rowSortingFeature, rowSelectionFeature })
25
+ const cols: ColumnDef<typeof features, Row>[] = [
26
+ { field: 'name', header: 'Name', width: 160 },
27
+ { field: 'id', header: 'Id', width: 80 },
28
+ ]
29
+
30
+ const tick = () => new Promise<void>((r) => setTimeout(r))
31
+ const rowsOf = (n: number): Row[] =>
32
+ Array.from({ length: n }, (_, i) => ({ id: i, name: `Row ${i}` }))
33
+
34
+ function mountGrid(extra: Record<string, unknown>, data: Row[] = rowsOf(5)) {
35
+ return new Promise<{
36
+ api: SvGridApi<typeof features, Row>
37
+ target: HTMLElement
38
+ destroy: () => void
39
+ }>((res, rej) => {
40
+ const target = document.createElement('div')
41
+ document.body.appendChild(target)
42
+ const app = mount(SvGrid, {
43
+ target,
44
+ props: {
45
+ data,
46
+ columns: cols,
47
+ features,
48
+ _rowModels: { coreRowModel: createCoreRowModel() },
49
+ getRowId: (r: Row) => String(r.id),
50
+ containerHeight: 200,
51
+ virtualization: false,
52
+ onApiReady(api: SvGridApi<typeof features, Row>) {
53
+ res({ api, target, destroy: () => { unmount(app); target.remove() } })
54
+ },
55
+ ...extra,
56
+ } as any,
57
+ })
58
+ queueMicrotask(() => {
59
+ if (!target.querySelector('[role="grid"]')) rej(new Error('no grid'))
60
+ })
61
+ })
62
+ }
63
+
64
+ describe('onVisibleRangeChange', () => {
65
+ it('reports the whole list when virtualization is off', async () => {
66
+ const seen: Array<{ startIndex: number; endIndex: number }> = []
67
+ const { destroy } = await mountGrid({ onVisibleRangeChange: (r: any) => seen.push(r) }, rowsOf(5))
68
+ await tick()
69
+
70
+ expect(seen.at(-1)).toEqual({ startIndex: 0, endIndex: 4 })
71
+ destroy()
72
+ })
73
+
74
+ it('reports row indices, not pixels, when virtualization is on', async () => {
75
+ const seen: Array<{ startIndex: number; endIndex: number }> = []
76
+ const { destroy } = await mountGrid(
77
+ {
78
+ onVisibleRangeChange: (r: any) => seen.push(r),
79
+ virtualization: true,
80
+ containerHeight: 200,
81
+ },
82
+ rowsOf(10_000),
83
+ )
84
+ await tick()
85
+
86
+ const last = seen.at(-1)!
87
+ expect(last.startIndex).toBe(0)
88
+ // A 200px viewport cannot be showing ten thousand rows, and the numbers
89
+ // are row indices - if these were the pixel offsets the old deriveds
90
+ // carry, endIndex would be in the hundreds.
91
+ expect(last.endIndex).toBeGreaterThan(0)
92
+ expect(last.endIndex).toBeLessThan(200)
93
+ destroy()
94
+ })
95
+
96
+ it('does not fire again while the range is unchanged', async () => {
97
+ const seen: Array<unknown> = []
98
+ const { api, destroy } = await mountGrid({ onVisibleRangeChange: (r: any) => seen.push(r) })
99
+ await tick()
100
+ const count = seen.length
101
+
102
+ // Something unrelated changes; the visible rows do not.
103
+ api.selectRows(['1'])
104
+ await tick()
105
+ expect(seen).toHaveLength(count)
106
+ destroy()
107
+ })
108
+ })
109
+
110
+ describe('rowPlaceholder', () => {
111
+ it('draws a skeleton cell per column for a loading row', async () => {
112
+ const { target, destroy } = await mountGrid({
113
+ rowPlaceholder: (row: Row) => (row.id === 2 ? 'loading' : null),
114
+ })
115
+ await tick()
116
+
117
+ const placeholder = target.querySelector('.sv-grid-placeholder-row')!
118
+ expect(placeholder).not.toBeNull()
119
+ expect(placeholder.getAttribute('aria-busy')).toBe('true')
120
+ // One shimmer per real column, so the table keeps its shape.
121
+ expect(placeholder.querySelectorAll('.sv-grid-placeholder-skeleton')).toHaveLength(cols.length)
122
+ // And the rows around it are untouched.
123
+ expect(target.querySelectorAll('.sv-grid-placeholder-row')).toHaveLength(1)
124
+ destroy()
125
+ })
126
+
127
+ it('draws one full-width message with a Retry button for a failed row', async () => {
128
+ const retried: Array<{ row: Row; index: number }> = []
129
+ const { target, destroy } = await mountGrid({
130
+ rowPlaceholder: (row: Row) => (row.id === 3 ? 'failed' : null),
131
+ onRetryRow: (row: Row, index: number) => retried.push({ row, index }),
132
+ })
133
+ await tick()
134
+
135
+ const failed = target.querySelector('.sv-grid-placeholder-failed')!
136
+ expect(failed).not.toBeNull()
137
+ expect(failed.textContent).toContain('Could not load')
138
+ expect(failed.querySelectorAll('.sv-grid-placeholder-skeleton')).toHaveLength(0)
139
+
140
+ const retry = failed.querySelector<HTMLButtonElement>('.sv-grid-placeholder-retry')!
141
+ retry.click()
142
+ expect(retried).toHaveLength(1)
143
+ expect(retried[0]!.row.id).toBe(3)
144
+ expect(retried[0]!.index).toBe(3)
145
+ destroy()
146
+ })
147
+
148
+ it('gives a run of failed rows one message, and a band under it', async () => {
149
+ // A failed block is claimed row by row; the message and Retry sit on
150
+ // the first row of the run, the rest are the same tint with nothing
151
+ // to read, so a hundred-row block is not a hundred red lines.
152
+ const { target, destroy } = await mountGrid(
153
+ {
154
+ rowPlaceholder: (row: Row) => (row.id >= 2 && row.id <= 4 ? 'failed' : null),
155
+ onRetryRow: () => {},
156
+ },
157
+ rowsOf(7),
158
+ )
159
+ await tick()
160
+ const failed = [...target.querySelectorAll('.sv-grid-placeholder-failed')]
161
+ expect(failed).toHaveLength(3)
162
+ expect(failed.map((r) => r.classList.contains('sv-grid-placeholder-failed-cont'))).toEqual([false, true, true])
163
+ expect(target.querySelectorAll('.sv-grid-placeholder-retry')).toHaveLength(1)
164
+ expect(failed[0]!.textContent).toContain('Could not load')
165
+ expect(failed[1]!.textContent!.trim()).toBe('')
166
+ // Every row of the run keeps its own slot: the scrollbar does not move.
167
+ expect(target.querySelectorAll('tbody .sv-grid-row').length).toBe(7)
168
+ destroy()
169
+ })
170
+
171
+ it('omits the Retry button when there is no handler to run', async () => {
172
+ const { target, destroy } = await mountGrid({
173
+ rowPlaceholder: (row: Row) => (row.id === 3 ? 'failed' : null),
174
+ })
175
+ await tick()
176
+ expect(target.querySelector('.sv-grid-placeholder-retry')).toBeNull()
177
+ destroy()
178
+ })
179
+
180
+ it('keeps placeholder rows out of select-all', async () => {
181
+ const { api, destroy } = await mountGrid({
182
+ showRowSelection: true,
183
+ rowPlaceholder: (row: Row) => (row.id >= 3 ? 'loading' : null),
184
+ })
185
+ await tick()
186
+
187
+ api.selectAllRows()
188
+ await tick()
189
+ // Selecting a row that has not loaded would leave an id that never
190
+ // resolves to anything.
191
+ expect(api.getSelectedRowIds().sort()).toEqual(['0', '1', '2'])
192
+ destroy()
193
+ })
194
+
195
+ it('changes nothing for a grid that does not use it', async () => {
196
+ const { target, destroy } = await mountGrid({})
197
+ await tick()
198
+ expect(target.querySelector('.sv-grid-placeholder-row')).toBeNull()
199
+ expect(target.querySelectorAll('.sv-grid-row').length).toBeGreaterThan(0)
200
+ destroy()
201
+ })
202
+ })
203
+
204
+ describe('rowSelectionModel', () => {
205
+ /**
206
+ * "Everything except these", the shape a server-side selection needs.
207
+ *
208
+ * Its state is a rune, so the header checkbox re-derives when the model
209
+ * changes. That is a requirement of the seam, not an accident of the test:
210
+ * the grid re-reads `headerState()` only when something reactive that it
211
+ * touched has changed.
212
+ */
213
+ function exceptModel(totalRows: number) {
214
+ const state = $state({ selectAll: false, toggled: new Set<string>() })
215
+ return {
216
+ state,
217
+ isSelected: (rowId: string) => state.selectAll !== state.toggled.has(rowId),
218
+ headerState: () => {
219
+ if (state.selectAll) return state.toggled.size === 0 ? 'all' : 'some'
220
+ return state.toggled.size === 0 ? 'none' : 'some'
221
+ },
222
+ toggle: (rowId: string) => {
223
+ const next = new Set(state.toggled)
224
+ if (next.has(rowId)) next.delete(rowId)
225
+ else next.add(rowId)
226
+ state.toggled = next
227
+ },
228
+ toggleAll: (next: boolean) => {
229
+ state.selectAll = next
230
+ state.toggled = new Set()
231
+ },
232
+ selectedCount: () => (state.selectAll ? totalRows - state.toggled.size : state.toggled.size),
233
+ }
234
+ }
235
+
236
+ it('answers the header checkbox instead of counting loaded rows', async () => {
237
+ const model = exceptModel(1_000_000)
238
+ const { target, destroy } = await mountGrid({ showRowSelection: true, rowSelectionModel: model })
239
+ await tick()
240
+
241
+ const header = target.querySelector('thead [role="checkbox"], thead input[type="checkbox"]')!
242
+ expect(header.getAttribute('aria-checked')).toBe('false')
243
+
244
+ model.toggleAll(true)
245
+ await vi.waitFor(() =>
246
+ expect(
247
+ target.querySelector('thead [role="checkbox"], thead input[type="checkbox"]')!
248
+ .getAttribute('aria-checked'),
249
+ ).toBe('true'),
250
+ )
251
+ // Five rows are loaded; a million are selected. The grid says "all"
252
+ // because the model said so, which is the whole point of the seam.
253
+ expect(model.selectedCount()).toBe(1_000_000)
254
+ destroy()
255
+ })
256
+
257
+ it('routes the header checkbox click to toggleAll, not to the loaded rows', async () => {
258
+ const model = exceptModel(1_000_000)
259
+ const { target, destroy } = await mountGrid({ showRowSelection: true, rowSelectionModel: model })
260
+ await tick()
261
+
262
+ const header = target.querySelector<HTMLElement>('thead [role="checkbox"], thead input[type="checkbox"]')!
263
+ header.click()
264
+ await tick()
265
+
266
+ expect(model.state.selectAll).toBe(true)
267
+ expect(model.selectedCount()).toBe(1_000_000)
268
+ destroy()
269
+ })
270
+
271
+ it('routes a row checkbox through the model', async () => {
272
+ const model = exceptModel(1_000_000)
273
+ const { api, destroy } = await mountGrid({ showRowSelection: true, rowSelectionModel: model })
274
+ await tick()
275
+
276
+ api.toggleRowSelected('2')
277
+ await tick()
278
+ expect(model.state.toggled.has('2')).toBe(true)
279
+ expect(model.isSelected('2')).toBe(true)
280
+
281
+ model.toggleAll(true)
282
+ await tick()
283
+ api.toggleRowSelected('2')
284
+ // Under select-all, toggling a row means "except this one".
285
+ expect(model.isSelected('2')).toBe(false)
286
+ expect(model.selectedCount()).toBe(999_999)
287
+ destroy()
288
+ })
289
+ })
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { describe, expect, it } from 'vitest'
7
7
  import { validateGridConfig } from './validate'
8
+ import { createRowPlaceholder } from './server-block-cache'
8
9
 
9
10
  type Row = { name: string; amount: number }
10
11
  const data: Row[] = [
@@ -48,6 +49,15 @@ describe('unknown field', () => {
48
49
  expect(run({ columns: [{ id: 'actions', header: '' }] })).toEqual([])
49
50
  })
50
51
 
52
+ it('looks past placeholder rows, which a row model shows before its data lands', () => {
53
+ const loading = [createRowPlaceholder('loading', { index: 0 }), createRowPlaceholder('loading', { index: 1 })]
54
+ // Only placeholders: nothing to judge the columns by, so no warning.
55
+ expect(run({ data: loading })).toEqual([])
56
+ // Placeholders ahead of real rows: the real rows are the sample.
57
+ expect(run({ data: [...loading, ...data] })).toEqual([])
58
+ expect(run({ data: [...loading, ...data], columns: [{ field: 'naem' }] })).toHaveLength(1)
59
+ })
60
+
51
61
  it('looks through group columns', () => {
52
62
  const msgs = run({ columns: [{ header: 'Group', columns: [{ field: 'nope' }] }] })
53
63
  expect(msgs[0]).toContain('"nope" does not exist')
package/src/validate.ts CHANGED
@@ -20,6 +20,7 @@
20
20
  * changes, and must not scale with row count.
21
21
  */
22
22
  import type { ColumnDef, RowData, TableFeatures } from './core'
23
+ import { rowPlaceholderState } from './server-block-cache'
23
24
 
24
25
  /** How many rows to sample when deciding whether a `field` exists. */
25
26
  const FIELD_SAMPLE_ROWS = 10
@@ -37,6 +38,8 @@ export type ValidateInput<TFeatures extends TableFeatures, TData extends RowData
37
38
  pageSize?: number
38
39
  groupBy?: ReadonlyArray<string>
39
40
  treeData?: { parentField?: string; idField?: string; column?: string }
41
+ /** Server-side grouping: group rows carry their key and aggregates, not the leaf fields. */
42
+ serverGroup?: unknown
40
43
  initialColumnPinning?: { left?: ReadonlyArray<string>; right?: ReadonlyArray<string> }
41
44
  columnVirtualization?: boolean
42
45
  externalPagination?: boolean
@@ -69,9 +72,13 @@ export function validateGridConfig<
69
72
  // ---- 1. A `field` that does not exist on the data -------------------------
70
73
  // The single most expensive silent failure: the column renders, and every
71
74
  // cell in it is blank. Sampled across several rows so genuinely sparse data
72
- // (a key absent from row 0 but present later) does not trip it.
73
- const sample = (input.data ?? []).slice(0, FIELD_SAMPLE_ROWS)
74
- if (sample.length) {
75
+ // (a key absent from row 0 but present later) does not trip it. A row
76
+ // model shows placeholder rows before its first block lands; those carry
77
+ // no data keys at all and say nothing about the columns.
78
+ const sample = (input.data ?? []).filter((row) => !rowPlaceholderState(row)).slice(0, FIELD_SAMPLE_ROWS)
79
+ // Under server-side grouping the first rows are group rows, which carry
80
+ // a key and aggregates and none of the leaf fields; nothing to judge by.
81
+ if (sample.length && !input.serverGroup) {
75
82
  const known = new Set<string>()
76
83
  for (const row of sample) {
77
84
  if (row && typeof row === 'object') for (const k of Object.keys(row)) known.add(k)