@svgrid/grid 3.0.3 → 3.0.5

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 (325) hide show
  1. package/README.md +6 -5
  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 +846 -207
  9. package/dist/SvGrid.css +178 -2
  10. package/dist/SvGrid.svelte +717 -720
  11. package/dist/SvGrid.types.d.ts +566 -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-CID62rlI.js +636 -0
  30. package/dist/cdn/GridMenus-DLL-qZeu.js +635 -0
  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-Bklofsad.js} +10615 -10155
  68. package/dist/cdn/{src-Bugf5XlI.js → src-jcFMkBnN.js} +7111 -6651
  69. package/dist/cdn/svgrid.js +27 -15
  70. package/dist/cdn/svgrid.svelte-external.js +27 -15
  71. package/dist/cdn/validate-C7Rn_Bns.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/gantt-view.svelte.d.ts +24 -0
  143. package/dist/gantt-view.svelte.js +13 -0
  144. package/dist/grid-icons.d.ts +2 -2
  145. package/dist/grid-icons.js +2 -2
  146. package/dist/grid-messages.d.ts +16 -3
  147. package/dist/grid-messages.js +10 -0
  148. package/dist/history.d.ts +83 -0
  149. package/dist/history.js +133 -0
  150. package/dist/index.d.ts +29 -10
  151. package/dist/index.js +23 -5
  152. package/dist/keyboard-handlers.js +88 -31
  153. package/dist/keyboard.d.ts +55 -0
  154. package/dist/keyboard.js +132 -0
  155. package/dist/menus.d.ts +3 -1
  156. package/dist/menus.js +7 -2
  157. package/dist/merges.d.ts +97 -0
  158. package/dist/merges.js +147 -0
  159. package/dist/row-model.d.ts +133 -0
  160. package/dist/row-model.js +16 -0
  161. package/dist/row-resize.d.ts +3 -0
  162. package/dist/row-resize.js +14 -0
  163. package/dist/scroll-sync.d.ts +1 -0
  164. package/dist/scroll-sync.js +24 -0
  165. package/dist/selection.d.ts +18 -1
  166. package/dist/selection.js +138 -18
  167. package/dist/server-block-cache.d.ts +214 -0
  168. package/dist/server-block-cache.js +531 -0
  169. package/dist/server-data-source.d.ts +240 -6
  170. package/dist/server-data-source.js +220 -20
  171. package/dist/server.d.ts +13 -0
  172. package/dist/server.js +13 -0
  173. package/dist/shortcut-registry.d.ts +145 -0
  174. package/dist/shortcut-registry.js +44 -0
  175. package/dist/svgrid-wrapper.types.d.ts +213 -22
  176. package/dist/validate.d.ts +4 -0
  177. package/dist/validate.js +21 -3
  178. package/dist/virtualization/virtualizer.js +38 -17
  179. package/package.json +11 -1
  180. package/src/GridMenus.svelte +44 -2
  181. package/src/SvChartMenu.svelte +69 -0
  182. package/src/SvChartPanes.svelte +113 -0
  183. package/src/SvChartPanes.test.ts +66 -0
  184. package/src/SvGrid.controller.svelte.ts +819 -179
  185. package/src/SvGrid.css +178 -2
  186. package/src/SvGrid.svelte +717 -720
  187. package/src/SvGrid.types.ts +564 -11
  188. package/src/SvGridCellEditor.svelte +17 -0
  189. package/src/SvGridChart.svelte +2257 -329
  190. package/src/SvGridChart.test.ts +1419 -1
  191. package/src/SvGridChart.types.ts +336 -0
  192. package/src/SvGridChartBuilder.svelte +323 -0
  193. package/src/SvGridChartPanel.svelte +221 -275
  194. package/src/SvGridChartPickers.svelte +417 -0
  195. package/src/SvGridChartView.svelte +13 -14
  196. package/src/SvModal.svelte +7 -1
  197. package/src/SvModal.test.ts +9 -0
  198. package/src/ai.test.ts +57 -2
  199. package/src/ai.ts +166 -22
  200. package/src/aria.d.ts +11 -0
  201. package/src/build-api.coverage.test.ts +184 -0
  202. package/src/build-api.ts +118 -22
  203. package/src/chart-axes.test.ts +573 -0
  204. package/src/chart-axes.ts +342 -0
  205. package/src/chart-cartesian.ts +1863 -0
  206. package/src/chart-decimate.test.ts +249 -0
  207. package/src/chart-decimate.ts +186 -0
  208. package/src/chart-export-pdf.test.ts +91 -0
  209. package/src/chart-export-pdf.ts +207 -0
  210. package/src/chart-export.test.ts +14 -0
  211. package/src/chart-export.ts +13 -3
  212. package/src/chart-financial.ts +208 -0
  213. package/src/chart-flow.ts +286 -0
  214. package/src/chart-format.ts +67 -0
  215. package/src/chart-grid.ts +179 -0
  216. package/src/chart-hierarchy.ts +135 -0
  217. package/src/chart-indicators.test.ts +443 -0
  218. package/src/chart-indicators.ts +384 -0
  219. package/src/chart-messages.test.ts +21 -0
  220. package/src/chart-messages.ts +184 -0
  221. package/src/chart-motion.test.ts +71 -0
  222. package/src/chart-motion.ts +148 -0
  223. package/src/chart-panel-messages.test.ts +29 -0
  224. package/src/chart-panel-messages.ts +550 -0
  225. package/src/chart-pivot.test.ts +54 -0
  226. package/src/chart-pivot.ts +225 -0
  227. package/src/chart-polar.ts +674 -0
  228. package/src/chart-samples.ts +106 -0
  229. package/src/chart-scale.ts +394 -0
  230. package/src/chart-series-types.test.ts +714 -0
  231. package/src/chart-stats.ts +409 -0
  232. package/src/chart-stream.test.ts +75 -0
  233. package/src/chart-stream.ts +114 -0
  234. package/src/chart-summary.test.ts +90 -0
  235. package/src/chart-summary.ts +197 -0
  236. package/src/chart-sync.svelte.ts +41 -0
  237. package/src/chart-table.test.ts +76 -0
  238. package/src/chart-table.ts +178 -0
  239. package/src/chart-trend.ts +47 -0
  240. package/src/chart-types.ts +1309 -0
  241. package/src/chart-validate.test.ts +100 -0
  242. package/src/chart-validate.ts +247 -0
  243. package/src/chart-zoom.test.ts +137 -0
  244. package/src/chart-zoom.ts +124 -0
  245. package/src/chart.coverage.test.ts +81 -0
  246. package/src/chart.test.ts +56 -3
  247. package/src/chart.ts +557 -2784
  248. package/src/clipboard.test.ts +151 -0
  249. package/src/clipboard.ts +145 -16
  250. package/src/column-resize.ts +7 -1
  251. package/src/columns.ts +2 -0
  252. package/src/command-context.test.ts +200 -0
  253. package/src/command-context.ts +137 -0
  254. package/src/core.rowmodel-cache.test.ts +51 -0
  255. package/src/core.ts +40 -12
  256. package/src/editing.test.ts +281 -3
  257. package/src/editing.ts +264 -38
  258. package/src/fill-patterns.test.ts +35 -0
  259. package/src/fill-patterns.ts +56 -2
  260. package/src/gantt-stub.test.svelte +38 -0
  261. package/src/gantt-view.svelte.ts +35 -0
  262. package/src/grid-icons.ts +2 -2
  263. package/src/grid-messages.ts +28 -3
  264. package/src/history.test.ts +196 -0
  265. package/src/history.ts +162 -0
  266. package/src/icon-seam.test.ts +5 -6
  267. package/src/index.ts +186 -12
  268. package/src/keyboard-handlers.coverage.test.ts +126 -6
  269. package/src/keyboard-handlers.ts +91 -30
  270. package/src/keyboard-shortcuts.seam.test.ts +200 -0
  271. package/src/keyboard.test.ts +124 -1
  272. package/src/keyboard.ts +142 -0
  273. package/src/menus.test.ts +26 -0
  274. package/src/menus.ts +10 -3
  275. package/src/merges.test.ts +112 -0
  276. package/src/merges.ts +168 -0
  277. package/src/pivot.grid.test.ts +40 -5
  278. package/src/row-model.ts +146 -0
  279. package/src/row-resize.test.ts +44 -0
  280. package/src/row-resize.ts +17 -0
  281. package/src/scroll-sync.test.ts +42 -1
  282. package/src/scroll-sync.ts +25 -0
  283. package/src/selection.multi-range.test.ts +34 -0
  284. package/src/selection.test.ts +33 -2
  285. package/src/selection.ts +143 -21
  286. package/src/server-block-cache.test.ts +645 -0
  287. package/src/server-block-cache.ts +677 -0
  288. package/src/server-data-source.infinite.test.ts +343 -0
  289. package/src/server-data-source.ts +450 -27
  290. package/src/server.ts +47 -0
  291. package/src/shortcut-registry.test.ts +97 -0
  292. package/src/shortcut-registry.ts +181 -0
  293. package/src/svgrid-wrapper.types.ts +184 -13
  294. package/src/svgrid.behavior.test.ts +135 -0
  295. package/src/svgrid.charting.test.ts +513 -3
  296. package/src/svgrid.context-menu.test.ts +31 -0
  297. package/src/svgrid.gantt-seam.test.ts +199 -0
  298. package/src/svgrid.new-features.wrapper.test.ts +10 -3
  299. package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
  300. package/src/svgrid.row-model-seam.svelte.test.ts +289 -0
  301. package/src/svgrid.upsell-license.test.ts +7 -4
  302. package/src/validate.test.ts +29 -0
  303. package/src/validate.ts +28 -3
  304. package/src/virtualization/virtualizer.ts +39 -17
  305. package/dist/SvGroupCell.svelte +0 -141
  306. package/dist/SvGroupCell.svelte.d.ts +0 -49
  307. package/dist/SvRowGroupPanel.svelte +0 -186
  308. package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
  309. package/dist/cdn/GridMenus-CHOMnW-c.js +0 -602
  310. package/dist/cdn/GridMenus-DhdPF700.js +0 -603
  311. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  312. package/dist/cdn/SvGridChart-1PMSp2LZ.js +0 -1481
  313. package/dist/cdn/SvGridChart-D02avhyr.js +0 -1480
  314. package/dist/cdn/SvGridChartPanel-DJnViOv4.js +0 -834
  315. package/dist/cdn/SvGridChartPanel-vPTNerFZ.js +0 -810
  316. package/dist/cdn/SvGridChartView-C--HhdUo.js +0 -56
  317. package/dist/cdn/SvGridChartView-KyyFG-wY.js +0 -55
  318. package/dist/cdn/chart-DLg3_zTQ.js +0 -1652
  319. package/dist/cdn/validate-_CDJzgIo.js +0 -75
  320. package/dist/server-group-model.d.ts +0 -98
  321. package/dist/server-group-model.js +0 -263
  322. package/src/SvGroupCell.svelte +0 -141
  323. package/src/SvRowGroupPanel.svelte +0 -186
  324. package/src/server-group-model.test.ts +0 -294
  325. package/src/server-group-model.ts +0 -370
@@ -1,186 +0,0 @@
1
- <!--
2
- A row-group panel (like the "drag columns to group" bar in enterprise grids).
3
- Shows the current group-by columns as chips you can remove or drag to reorder,
4
- plus a "+ Group by" menu to add one. It is also a drop target: a column drag
5
- that carries `text/sv-column` (its column id) adds that column. Wire `onChange`
6
- to a server group model's `setGroupBy`, or to client grouping.
7
-
8
- <SvRowGroupPanel
9
- columns={[{ id: 'region', label: 'Region' }, { id: 'country', label: 'Country' }]}
10
- groupBy={view.groupBy}
11
- onChange={(g) => ctl.setGroupBy(g)}
12
- />
13
- -->
14
- <script lang="ts">
15
- import { GRID_ICON_GLYPHS, type GridIconName, type GridIcons } from './grid-icons'
16
-
17
- type Col = { id: string; label: string }
18
- type Props = {
19
- /** Columns that can be grouped on. */
20
- columns: Col[]
21
- /** Current group-by column ids, outer to inner. */
22
- groupBy: string[]
23
- /** Called with the new group-by order. */
24
- onChange: (groupBy: string[]) => void
25
- /** Text shown when nothing is grouped. */
26
- placeholder?: string
27
- /**
28
- * Icon overrides, the same map `<SvGrid icons>` takes. This panel draws
29
- * `drag-handle`, `remove` and `breadcrumb-separator`. You mount it
30
- * yourself, so pass the same object you gave the grid to keep them
31
- * consistent.
32
- */
33
- icons?: GridIcons
34
- }
35
- let { columns, groupBy, onChange, placeholder = 'Drag a column here to group by it', icons }: Props = $props()
36
-
37
- const labelOf = (id: string) => columns.find((c) => c.id === id)?.label ?? id
38
- const available = $derived(columns.filter((c) => !groupBy.includes(c.id)))
39
-
40
- let dragIndex = $state<number | null>(null)
41
-
42
- function remove(id: string) {
43
- onChange(groupBy.filter((g) => g !== id))
44
- }
45
- function add(id: string) {
46
- if (id && !groupBy.includes(id)) onChange([...groupBy, id])
47
- }
48
- function reorder(to: number) {
49
- if (dragIndex === null || dragIndex === to) return
50
- const next = [...groupBy]
51
- const [moved] = next.splice(dragIndex, 1)
52
- next.splice(to, 0, moved!)
53
- dragIndex = null
54
- onChange(next)
55
- }
56
- function onPanelDrop(e: DragEvent) {
57
- const id = e.dataTransfer?.getData('text/sv-column')
58
- if (id) add(id)
59
- }
60
- /** Keyboard equivalent of dragging a chip: moves the chip at `i` one slot earlier/later,
61
- * reusing `reorder()` (via `dragIndex`) so there is a single source of truth for the move. */
62
- function moveByKeyboard(i: number, delta: number) {
63
- const to = i + delta
64
- if (to < 0 || to >= groupBy.length) return
65
- dragIndex = i
66
- reorder(to)
67
- }
68
- function onChipKeydown(e: KeyboardEvent, i: number) {
69
- if (!e.altKey) return
70
- if (e.key === 'ArrowLeft') {
71
- e.preventDefault()
72
- moveByKeyboard(i, -1)
73
- } else if (e.key === 'ArrowRight') {
74
- e.preventDefault()
75
- moveByKeyboard(i, 1)
76
- }
77
- }
78
- </script>
79
-
80
- <!-- Same override-or-default resolution SvGrid uses, kept local because this
81
- component is mounted by the consumer and never sees the grid's snippet. -->
82
- {#snippet ic(name: GridIconName)}
83
- {@const override = icons?.[name]}
84
- {#if override}{@render override()}{:else}{GRID_ICON_GLYPHS[name]}{/if}
85
- {/snippet}
86
-
87
- <div
88
- class="sv-rowgroup-panel"
89
- role="group"
90
- aria-label="Row groups"
91
- ondragover={(e) => e.preventDefault()}
92
- ondrop={onPanelDrop}
93
- >
94
- <span class="sv-rgp-label" aria-hidden="true">Group by:</span>
95
- {#if groupBy.length === 0}
96
- <span class="sv-rgp-empty">{placeholder}</span>
97
- {:else}
98
- {#each groupBy as id, i (id)}
99
- {#if i > 0}<span class="sv-rgp-sep" aria-hidden="true">{@render ic('breadcrumb-separator')}</span>{/if}
100
- <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
101
- <span
102
- class="sv-rgp-chip"
103
- class:sv-rgp-chip-drag={dragIndex === i}
104
- role="group"
105
- tabindex="0"
106
- aria-label={`Grouped by ${labelOf(id)}, position ${i + 1} of ${groupBy.length}. Press Alt+Arrow keys to reorder.`}
107
- draggable="true"
108
- ondragstart={() => (dragIndex = i)}
109
- ondragend={() => (dragIndex = null)}
110
- ondragover={(e) => e.preventDefault()}
111
- ondrop={() => reorder(i)}
112
- onkeydown={(e) => onChipKeydown(e, i)}
113
- >
114
- <span class="sv-rgp-grip" aria-hidden="true">{@render ic('drag-handle')}</span>
115
- {labelOf(id)}
116
- <button type="button" class="sv-rgp-x" onclick={() => remove(id)} aria-label={`Stop grouping by ${labelOf(id)}`}>{@render ic('remove')}</button>
117
- </span>
118
- {/each}
119
- {/if}
120
- {#if available.length}
121
- <select
122
- class="sv-rgp-add"
123
- aria-label="Add a group column"
124
- onchange={(e) => { add(e.currentTarget.value); e.currentTarget.value = '' }}
125
- >
126
- <option value="">+ Group by</option>
127
- {#each available as c (c.id)}<option value={c.id}>{c.label}</option>{/each}
128
- </select>
129
- {/if}
130
- </div>
131
-
132
- <style>
133
- .sv-rowgroup-panel {
134
- display: flex;
135
- align-items: center;
136
- flex-wrap: wrap;
137
- gap: 6px;
138
- padding: 8px 10px;
139
- border: 1px dashed var(--sg-border, #cbd5e1);
140
- border-radius: 10px;
141
- background: var(--sg-header-bg, #f8fafc);
142
- font-size: 13px;
143
- }
144
- .sv-rgp-label { color: var(--sg-muted, #64748b); font-weight: 600; }
145
- .sv-rgp-empty { color: var(--sg-muted, #94a3b8); font-style: italic; }
146
- .sv-rgp-sep { color: var(--sg-muted, #94a3b8); }
147
- .sv-rgp-chip {
148
- display: inline-flex;
149
- align-items: center;
150
- gap: 6px;
151
- padding: 3px 6px 3px 8px;
152
- border-radius: 999px;
153
- background: var(--sg-bg, #fff);
154
- border: 1px solid var(--sg-border, #e2e8f0);
155
- cursor: grab;
156
- user-select: none;
157
- font-weight: 600;
158
- }
159
- .sv-rgp-chip-drag { opacity: 0.5; }
160
- .sv-rgp-grip { color: var(--sg-muted, #94a3b8); cursor: grab; }
161
- .sv-rgp-x {
162
- display: inline-flex;
163
- align-items: center;
164
- justify-content: center;
165
- width: 16px;
166
- height: 16px;
167
- border: 0;
168
- border-radius: 50%;
169
- background: none;
170
- color: var(--sg-muted, #64748b);
171
- cursor: pointer;
172
- font-size: 14px;
173
- line-height: 1;
174
- }
175
- .sv-rgp-x:hover { background: var(--sg-header-bg, #f1f5f9); color: var(--sg-fg, #0f172a); }
176
- .sv-rgp-add {
177
- font: inherit;
178
- font-size: 13px;
179
- padding: 4px 8px;
180
- border-radius: 8px;
181
- border: 1px solid var(--sg-border, #e2e8f0);
182
- background: var(--sg-bg, #fff);
183
- color: var(--sg-accent, #2563eb);
184
- cursor: pointer;
185
- }
186
- </style>
@@ -1,294 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { createServerGroupModel } from './server-group-model'
3
- import type { ServerDataSource, ServerDisplayRow, ServerGroupRow } from './server-data-source'
4
-
5
- type Row = { country: string; city: string; amount: number }
6
-
7
- const DATA: Row[] = [
8
- { country: 'DE', city: 'Berlin', amount: 10 },
9
- { country: 'DE', city: 'Berlin', amount: 5 },
10
- { country: 'DE', city: 'Munich', amount: 7 },
11
- { country: 'FR', city: 'Paris', amount: 3 },
12
- ]
13
-
14
- /**
15
- * A fake backend that groups + aggregates DATA per the request's groupBy /
16
- * groupKeys, exactly as a real SQL GROUP BY would - returns group rows for a
17
- * group level and leaf rows at the bottom.
18
- */
19
- function makeGroupSource(data: Row[], delay = 0): ServerDataSource<Row> & { calls: number } {
20
- return {
21
- calls: 0,
22
- async getRows(req) {
23
- this.calls += 1
24
- if (delay) await new Promise((r) => setTimeout(r, delay))
25
- const subset = data.filter((r) =>
26
- req.groupKeys!.every((k, i) => String((r as Record<string, unknown>)[req.groupBy![i]!]) === k),
27
- )
28
- const level = req.groupKeys!.length
29
- if (level < req.groupBy!.length) {
30
- const field = req.groupBy![level]!
31
- const map = new Map<string, Record<string, unknown>>()
32
- for (const r of subset) {
33
- const key = String((r as Record<string, unknown>)[field])
34
- const agg = map.get(key) ?? { [field]: (r as Record<string, unknown>)[field] }
35
- for (const a of req.aggregations!) {
36
- const cur = (agg[a.col] as number) ?? 0
37
- agg[a.col] = a.fn === 'count' ? cur + 1 : cur + ((r as Record<string, unknown>)[a.col] as number)
38
- }
39
- map.set(key, agg)
40
- }
41
- const groups = [...map.values()]
42
- return { rows: groups.slice(req.startRow, req.endRow) as unknown as Row[], rowCount: groups.length }
43
- }
44
- return { rows: subset.slice(req.startRow, req.endRow), rowCount: subset.length }
45
- },
46
- }
47
- }
48
-
49
- const flush = () => new Promise((r) => setTimeout(r, 0))
50
- const groups = (rows: ServerDisplayRow<Row>[]) => rows.filter((r): r is ServerGroupRow<Row> => r.kind === 'group')
51
-
52
- describe('createServerGroupModel', () => {
53
- it('refresh() loads the top-level groups with aggregates', async () => {
54
- const src = makeGroupSource(DATA)
55
- let state = createServerGroupModel<Row>(src, {
56
- groupBy: ['country', 'city'],
57
- aggregations: [{ col: 'amount', fn: 'sum' }],
58
- onChange: (s) => (state = s as never),
59
- }).getState()
60
- const ctl = createServerGroupModel<Row>(src, {
61
- groupBy: ['country', 'city'],
62
- aggregations: [{ col: 'amount', fn: 'sum' }],
63
- onChange: (s) => (state = s as never),
64
- })
65
- ctl.refresh()
66
- await flush()
67
- const g = groups(state.displayRows)
68
- expect(g.map((r) => r.key)).toEqual(['DE', 'FR'])
69
- expect(g.every((r) => r.level === 0 && !r.expanded)).toBe(true)
70
- expect(g[0]!.aggregates.amount).toBe(22) // 10 + 5 + 7
71
- expect(g[1]!.aggregates.amount).toBe(3)
72
- })
73
-
74
- it('expanding a group lazily fetches and splices its child groups', async () => {
75
- const src = makeGroupSource(DATA)
76
- let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
77
- const ctl = createServerGroupModel<Row>(src, {
78
- groupBy: ['country', 'city'],
79
- aggregations: [{ col: 'amount', fn: 'sum' }],
80
- onChange: (s) => (state = s as never),
81
- })
82
- ctl.refresh()
83
- await flush()
84
- const callsBefore = src.calls
85
- ctl.expandGroup(['DE'])
86
- await flush()
87
- expect(src.calls).toBe(callsBefore + 1) // one fetch for DE's children
88
- const rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
89
- // DE (expanded), Berlin, Munich, FR
90
- expect(rows.map((r) => (r.kind === 'group' ? r.key : 'leaf'))).toEqual(['DE', 'Berlin', 'Munich', 'FR'])
91
- const berlin = rows.find((r) => r.kind === 'group' && r.key === 'Berlin') as ServerGroupRow<Row>
92
- expect(berlin.level).toBe(1)
93
- expect(berlin.aggregates.amount).toBe(15) // 10 + 5
94
- })
95
-
96
- it('expanding a bottom-level group returns leaf rows', async () => {
97
- const src = makeGroupSource(DATA)
98
- let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
99
- const ctl = createServerGroupModel<Row>(src, {
100
- groupBy: ['country', 'city'],
101
- onChange: (s) => (state = s as never),
102
- })
103
- ctl.refresh()
104
- await flush()
105
- ctl.expandGroup(['DE'])
106
- await flush()
107
- ctl.expandGroup(['DE', 'Berlin'])
108
- await flush()
109
- const rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
110
- const leaves = rows.filter((r) => r.kind === 'leaf')
111
- expect(leaves).toHaveLength(2) // the two Berlin rows
112
- expect(leaves.every((r) => r.level === 2)).toBe(true)
113
- })
114
-
115
- it('collapse removes the children again', async () => {
116
- const src = makeGroupSource(DATA)
117
- // Reads go through ctl.getState() below, so the change callback is a no-op.
118
- const ctl = createServerGroupModel<Row>(src, {
119
- groupBy: ['country', 'city'],
120
- onChange: () => {},
121
- })
122
- ctl.refresh()
123
- await flush()
124
- ctl.expandGroup(['DE'])
125
- await flush()
126
- ctl.collapseGroup(['DE'])
127
- const rows = ctl.getState().displayRows
128
- expect(rows.map((r) => (r.kind === 'group' ? r.key : 'leaf'))).toEqual(['DE', 'FR'])
129
- expect(ctl.isExpanded(['DE'])).toBe(false)
130
- })
131
-
132
- it('setFilter resets the tree and refetches from the top', async () => {
133
- const src = makeGroupSource(DATA)
134
- const ctl = createServerGroupModel<Row>(src, { groupBy: ['country'], onChange: () => {} })
135
- ctl.refresh()
136
- await flush()
137
- const before = src.calls
138
- ctl.setFilter({ global: 'x' })
139
- await flush()
140
- expect(src.calls).toBeGreaterThan(before)
141
- expect(ctl.getState().filterModel).toEqual({ global: 'x' })
142
- })
143
-
144
- it('a stale expand response is dropped after a reset (race safety)', async () => {
145
- const src = makeGroupSource(DATA, 20) // slow
146
- let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
147
- const ctl = createServerGroupModel<Row>(src, {
148
- groupBy: ['country', 'city'],
149
- onChange: (s) => (state = s as never),
150
- })
151
- ctl.refresh()
152
- await new Promise((r) => setTimeout(r, 30))
153
- ctl.expandGroup(['DE']) // in-flight, slow
154
- ctl.setGroupBy(['city']) // reset (bumps generation) before DE's children land
155
- await new Promise((r) => setTimeout(r, 60))
156
- const rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
157
- // Regrouped by city at the top; no stale DE subtree leaked in.
158
- expect(rows.every((r) => r.level === 0)).toBe(true)
159
- expect(rows.some((r) => r.kind === 'group' && r.key === 'DE')).toBe(false)
160
- })
161
-
162
- it('pages a group\'s children in blocks (load more)', async () => {
163
- const data: Row[] = [
164
- { country: 'DE', city: 'B', amount: 1 },
165
- { country: 'DE', city: 'M', amount: 2 },
166
- { country: 'DE', city: 'H', amount: 3 },
167
- { country: 'DE', city: 'K', amount: 4 },
168
- ]
169
- const src = makeGroupSource(data)
170
- let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
171
- const ctl = createServerGroupModel<Row>(src, {
172
- groupBy: ['country'],
173
- pageSize: 2,
174
- onChange: (s) => (state = s as never),
175
- })
176
- ctl.refresh()
177
- await flush()
178
- ctl.expandGroup(['DE'])
179
- await flush()
180
- let rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
181
- expect(rows.filter((r) => r.kind === 'leaf')).toHaveLength(2) // first block
182
- const more = rows.find((r) => r.kind === 'more')
183
- expect(more && more.kind === 'more' && more.remaining).toBe(2)
184
-
185
- ctl.loadMoreChildren(['DE'])
186
- await flush()
187
- rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
188
- expect(rows.filter((r) => r.kind === 'leaf')).toHaveLength(4) // all loaded
189
- expect(rows.some((r) => r.kind === 'more')).toBe(false) // no more affordance
190
- })
191
-
192
- it('emits a subtotal footer row after an expanded group when groupFooters is on', async () => {
193
- const src = makeGroupSource(DATA)
194
- let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
195
- const ctl = createServerGroupModel<Row>(src, {
196
- groupBy: ['country', 'city'],
197
- aggregations: [{ col: 'amount', fn: 'sum' }],
198
- groupFooters: true,
199
- onChange: (s) => (state = s as never),
200
- })
201
- ctl.refresh()
202
- await flush()
203
- ctl.expandGroup(['DE'])
204
- await flush()
205
- const rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
206
- const footers = rows.filter((r) => r.kind === 'footer')
207
- expect(footers).toHaveLength(1)
208
- expect(footers[0]!.kind === 'footer' && footers[0]!.key).toBe('DE')
209
- })
210
-
211
- it('shows skeleton rows while a group\'s first block is loading', async () => {
212
- const src = makeGroupSource(DATA, 20) // slow
213
- let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
214
- const ctl = createServerGroupModel<Row>(src, {
215
- groupBy: ['country', 'city'],
216
- skeletonRows: 3,
217
- onChange: (s) => (state = s as never),
218
- })
219
- ctl.refresh()
220
- await new Promise((r) => setTimeout(r, 35)) // root loaded
221
- ctl.expandGroup(['DE']) // fetch in flight -> synchronous emit with skeletons
222
- const loading = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
223
- expect(loading.filter((r) => r.kind === 'skeleton')).toHaveLength(3)
224
- await new Promise((r) => setTimeout(r, 40)) // DE loads
225
- expect((state as { displayRows: ServerDisplayRow<Row>[] }).displayRows.some((r) => r.kind === 'skeleton')).toBe(false)
226
- })
227
- })
228
-
229
- type Node = { id: string; parentId: string | null; name: string; kids: boolean }
230
- const TREE: Node[] = [
231
- { id: '1', parentId: null, name: 'Root A', kids: true },
232
- { id: '2', parentId: null, name: 'Root B', kids: false },
233
- { id: '1a', parentId: '1', name: 'Child A1', kids: true },
234
- { id: '1b', parentId: '1', name: 'Child A2', kids: false },
235
- { id: '1a1', parentId: '1a', name: 'Leaf A1a', kids: false },
236
- ]
237
-
238
- /** A fake self-referential tree backend: return the children of the node at the end of groupKeys. */
239
- function makeTreeSource(nodes: Node[]): ServerDataSource<Node> & { calls: number } {
240
- return {
241
- calls: 0,
242
- async getRows(req) {
243
- this.calls += 1
244
- const parent = req.groupKeys!.length ? req.groupKeys![req.groupKeys!.length - 1] : null
245
- const children = nodes.filter((n) => (n.parentId ?? null) === (parent ?? null))
246
- return { rows: children, rowCount: children.length }
247
- },
248
- }
249
- }
250
-
251
- describe('createServerGroupModel - treeData', () => {
252
- const opts = (onChange: (s: unknown) => void) => ({
253
- treeData: true,
254
- getRowId: (n: Node) => n.id,
255
- hasChildren: (n: Node) => n.kids,
256
- onChange: onChange as never,
257
- })
258
- const nameOf = (r: ServerDisplayRow<Node>) => ('data' in r ? r.data.name : `(${r.kind})`)
259
-
260
- it('throws without getRowId/hasChildren', () => {
261
- expect(() => createServerGroupModel<Node>(makeTreeSource(TREE), { treeData: true, onChange: () => {} })).toThrow()
262
- })
263
-
264
- it('loads roots, marking expandable nodes as groups and leaves as leaves', async () => {
265
- const src = makeTreeSource(TREE)
266
- let state = { displayRows: [] as ServerDisplayRow<Node>[] } as never
267
- const ctl = createServerGroupModel<Node>(src, opts((s) => (state = s as never)))
268
- ctl.refresh()
269
- await flush()
270
- const rows = (state as { displayRows: ServerDisplayRow<Node>[] }).displayRows
271
- expect(rows.map((r) => [r.kind, nameOf(r)])).toEqual([
272
- ['group', 'Root A'], // kids: true
273
- ['leaf', 'Root B'], // kids: false
274
- ])
275
- })
276
-
277
- it('expanding a node fetches and splices its children by id path', async () => {
278
- const src = makeTreeSource(TREE)
279
- let state = { displayRows: [] as ServerDisplayRow<Node>[] } as never
280
- const ctl = createServerGroupModel<Node>(src, opts((s) => (state = s as never)))
281
- ctl.refresh()
282
- await flush()
283
- ctl.expandGroup(['1'])
284
- await flush()
285
- let rows = (state as { displayRows: ServerDisplayRow<Node>[] }).displayRows
286
- expect(rows.map(nameOf)).toEqual(['Root A', 'Child A1', 'Child A2', 'Root B'])
287
- // drill one more level: Child A1 -> Leaf A1a
288
- ctl.expandGroup(['1', '1a'])
289
- await flush()
290
- rows = (state as { displayRows: ServerDisplayRow<Node>[] }).displayRows
291
- expect(rows.map(nameOf)).toEqual(['Root A', 'Child A1', 'Leaf A1a', 'Child A2', 'Root B'])
292
- expect(rows.find((r) => nameOf(r) === 'Leaf A1a')!.level).toBe(2)
293
- })
294
- })