@svgrid/grid 2.6.19 → 2.6.21

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 (65) hide show
  1. package/dist/SvGrid.controller.svelte.d.ts +1 -0
  2. package/dist/SvGrid.controller.svelte.js +64 -2
  3. package/dist/SvGrid.svelte +2 -1
  4. package/dist/SvGrid.types.d.ts +150 -0
  5. package/dist/ai.d.ts +28 -0
  6. package/dist/ai.js +6 -0
  7. package/dist/cdn/{GridMenus-B0F9iBrG.js → GridMenus-BfTAKn84.js} +1 -1
  8. package/dist/cdn/{GridMenus-IHK_l7m6.js → GridMenus-C3bJd7w8.js} +1 -1
  9. package/dist/cdn/{src-Cd0tearp.js → src-BYq-qyrp.js} +1012 -999
  10. package/dist/cdn/{src-B1TdiyS8.js → src-DBel9wRZ.js} +1324 -1311
  11. package/dist/cdn/svgrid.js +1 -1
  12. package/dist/cdn/svgrid.svelte-external.js +1 -1
  13. package/dist/cdn/validate-_CDJzgIo.js +75 -0
  14. package/dist/cell-formatting.d.ts +2 -0
  15. package/dist/cell-formatting.js +2 -0
  16. package/dist/chart-export.d.ts +1 -0
  17. package/dist/chart.d.ts +31 -5
  18. package/dist/chart.js +9 -3
  19. package/dist/core.d.ts +197 -0
  20. package/dist/core.js +72 -0
  21. package/dist/createTree.svelte.d.ts +3 -0
  22. package/dist/createTree.svelte.js +1 -0
  23. package/dist/datetime/date-core.d.ts +2 -0
  24. package/dist/datetime/date-restrict.d.ts +1 -0
  25. package/dist/datetime/timezone.d.ts +1 -0
  26. package/dist/dock-manager-model.d.ts +3 -0
  27. package/dist/dock-manager-model.js +1 -0
  28. package/dist/dock-model.d.ts +6 -0
  29. package/dist/dock-model.js +3 -0
  30. package/dist/editor-contract.d.ts +1 -0
  31. package/dist/list-option.d.ts +1 -0
  32. package/dist/positioning.d.ts +2 -0
  33. package/dist/scheduler-ical.d.ts +1 -0
  34. package/dist/scheduler-model.d.ts +1 -0
  35. package/dist/summaries.js +22 -0
  36. package/dist/svgrid-wrapper.types.d.ts +5 -0
  37. package/dist/toast-store.svelte.d.ts +4 -0
  38. package/dist/validate.d.ts +50 -0
  39. package/dist/validate.js +187 -0
  40. package/package.json +4 -1
  41. package/src/SvGrid.controller.svelte.ts +68 -2
  42. package/src/SvGrid.svelte +2 -1
  43. package/src/SvGrid.types.ts +150 -0
  44. package/src/ai.ts +28 -0
  45. package/src/cell-formatting.ts +2 -0
  46. package/src/chart-export.ts +1 -0
  47. package/src/chart.ts +31 -5
  48. package/src/core.ts +207 -0
  49. package/src/createTree.svelte.ts +3 -0
  50. package/src/datetime/date-core.ts +2 -0
  51. package/src/datetime/date-restrict.ts +1 -0
  52. package/src/datetime/timezone.ts +1 -0
  53. package/src/dock-manager-model.ts +3 -0
  54. package/src/dock-model.ts +6 -0
  55. package/src/editor-contract.ts +1 -0
  56. package/src/list-option.ts +1 -0
  57. package/src/positioning.ts +2 -0
  58. package/src/scheduler-ical.ts +1 -0
  59. package/src/scheduler-model.ts +1 -0
  60. package/src/summaries.ts +21 -0
  61. package/src/svgrid-wrapper.types.ts +5 -0
  62. package/src/svgrid.summaries.test.ts +217 -0
  63. package/src/toast-store.svelte.ts +4 -0
  64. package/src/validate.test.ts +207 -0
  65. package/src/validate.ts +269 -0
@@ -0,0 +1,217 @@
1
+ /**
2
+ * The footer summary row: its default, its `summary` shortcut, and the
3
+ * per-column `summary` aggregator.
4
+ *
5
+ * The default matters more than it looks. It used to be ON, so a plain
6
+ * `<SvGrid {data} {columns} />` grew a totals row nobody asked for and every
7
+ * caller had to opt out - which is why this repo had 375 `enableRowSummaries=
8
+ * {false}` call sites against 6 that wanted it. These tests pin the flip.
9
+ */
10
+ import { describe, expect, it } from 'vitest'
11
+ import { mount, unmount } from 'svelte'
12
+ import SvGrid from './SvGrid.svelte'
13
+ import {
14
+ columnFilteringFeature,
15
+ createCoreRowModel,
16
+ createFilteredRowModel,
17
+ createPaginatedRowModel,
18
+ createSortedRowModel,
19
+ rowPaginationFeature,
20
+ rowSelectionFeature,
21
+ rowSortingFeature,
22
+ sortFns,
23
+ tableFeatures,
24
+ type ColumnDef,
25
+ } from './index'
26
+
27
+ const features = tableFeatures({
28
+ columnFilteringFeature,
29
+ rowPaginationFeature,
30
+ rowSelectionFeature,
31
+ rowSortingFeature,
32
+ })
33
+
34
+ type Row = { id: number; name: string; amount: number }
35
+
36
+ const rows: Row[] = [
37
+ { id: 1, name: 'Ada', amount: 100 },
38
+ { id: 2, name: 'Grace', amount: 200 },
39
+ { id: 3, name: 'Alan', amount: 300 },
40
+ ]
41
+
42
+ const baseColumns: ColumnDef<typeof features, Row>[] = [
43
+ { field: 'name', header: 'Name', width: 200 },
44
+ { field: 'amount', header: 'Amount', width: 160 },
45
+ ]
46
+
47
+ function mountGrid(overrides: Record<string, unknown> = {}) {
48
+ const target = document.createElement('div')
49
+ target.style.width = '900px'
50
+ target.style.height = '400px'
51
+ document.body.appendChild(target)
52
+
53
+ const app = mount(SvGrid, {
54
+ target,
55
+ props: {
56
+ data: rows,
57
+ columns: baseColumns,
58
+ features,
59
+ _rowModels: {
60
+ coreRowModel: createCoreRowModel(),
61
+ filteredRowModel: createFilteredRowModel(),
62
+ sortedRowModel: createSortedRowModel(sortFns),
63
+ paginatedRowModel: createPaginatedRowModel(),
64
+ },
65
+ rowHeight: 36,
66
+ containerHeight: 320,
67
+ virtualization: false,
68
+ ...overrides,
69
+ } as any,
70
+ })
71
+
72
+ return {
73
+ target,
74
+ row: () => target.querySelector('.sv-grid-summary-row'),
75
+ cells: () =>
76
+ [...target.querySelectorAll('.sv-grid-summary-column')].map((c) =>
77
+ (c.textContent ?? '').trim(),
78
+ ),
79
+ destroy: () => {
80
+ unmount(app)
81
+ target.remove()
82
+ },
83
+ }
84
+ }
85
+
86
+ describe('SvGrid - footer summary row', () => {
87
+ it('is OFF by default', () => {
88
+ const g = mountGrid()
89
+ try {
90
+ expect(g.row()).toBeNull()
91
+ } finally {
92
+ g.destroy()
93
+ }
94
+ })
95
+
96
+ it('turns on with the `summary` shortcut', () => {
97
+ const g = mountGrid({ summary: true })
98
+ try {
99
+ expect(g.row()).not.toBeNull()
100
+ } finally {
101
+ g.destroy()
102
+ }
103
+ })
104
+
105
+ it('still turns on with the long-form `enableRowSummaries`', () => {
106
+ const g = mountGrid({ enableRowSummaries: true })
107
+ try {
108
+ expect(g.row()).not.toBeNull()
109
+ } finally {
110
+ g.destroy()
111
+ }
112
+ })
113
+
114
+ it('lets the `summary` shortcut win over the long-form prop', () => {
115
+ const on = mountGrid({ summary: true, enableRowSummaries: false })
116
+ try {
117
+ expect(on.row()).not.toBeNull()
118
+ } finally {
119
+ on.destroy()
120
+ }
121
+
122
+ const off = mountGrid({ summary: false, enableRowSummaries: true })
123
+ try {
124
+ expect(off.row()).toBeNull()
125
+ } finally {
126
+ off.destroy()
127
+ }
128
+ })
129
+
130
+ it('defaults to summing a numeric column and counting the rest', () => {
131
+ const g = mountGrid({ summary: true })
132
+ try {
133
+ const cells = g.cells()
134
+ expect(cells).toContain('Count: 3')
135
+ expect(cells.join(' ')).toContain('600')
136
+ } finally {
137
+ g.destroy()
138
+ }
139
+ })
140
+ })
141
+
142
+ describe('SvGrid - per-column summary aggregator', () => {
143
+ it('honours an explicit aggregator instead of the default sum', () => {
144
+ const g = mountGrid({
145
+ summary: true,
146
+ columns: [
147
+ { field: 'name', header: 'Name', width: 200 },
148
+ { field: 'amount', header: 'Amount', width: 160, summary: 'avg' },
149
+ ],
150
+ })
151
+ try {
152
+ // avg of 100 / 200 / 300, not the 600 the default would show.
153
+ expect(g.cells().join(' ')).toContain('200')
154
+ expect(g.cells().join(' ')).not.toContain('600')
155
+ } finally {
156
+ g.destroy()
157
+ }
158
+ })
159
+
160
+ it('supports min, max and count', () => {
161
+ for (const [agg, expected] of [
162
+ ['min', '100'],
163
+ ['max', '300'],
164
+ ['count', '3'],
165
+ ] as const) {
166
+ const g = mountGrid({
167
+ summary: true,
168
+ columns: [
169
+ { field: 'name', header: 'Name', width: 200 },
170
+ { field: 'amount', header: 'Amount', width: 160, summary: agg },
171
+ ],
172
+ })
173
+ try {
174
+ expect(g.cells().join(' ')).toContain(expected)
175
+ } finally {
176
+ g.destroy()
177
+ }
178
+ }
179
+ })
180
+
181
+ it('takes a custom aggregator function', () => {
182
+ const g = mountGrid({
183
+ summary: true,
184
+ columns: [
185
+ { field: 'name', header: 'Name', width: 200 },
186
+ {
187
+ field: 'amount',
188
+ header: 'Amount',
189
+ width: 160,
190
+ summary: (values: number[]) => `${values.length} values`,
191
+ },
192
+ ],
193
+ })
194
+ try {
195
+ expect(g.cells()).toContain('3 values')
196
+ } finally {
197
+ g.destroy()
198
+ }
199
+ })
200
+
201
+ it('leaves the cell blank on `summary: false`', () => {
202
+ const g = mountGrid({
203
+ summary: true,
204
+ columns: [
205
+ { field: 'name', header: 'Name', width: 200, summary: false },
206
+ { field: 'amount', header: 'Amount', width: 160 },
207
+ ],
208
+ })
209
+ try {
210
+ // The name column would otherwise read "Count: 3".
211
+ expect(g.cells()).not.toContain('Count: 3')
212
+ expect(g.cells().join(' ')).toContain('600')
213
+ } finally {
214
+ g.destroy()
215
+ }
216
+ })
217
+ })
@@ -16,6 +16,7 @@
16
16
  import type { Snippet } from 'svelte'
17
17
  import { announce } from './a11y/live-region'
18
18
 
19
+ /** A toast's severity, which selects its colour and icon. */
19
20
  export type ToastVariant = 'info' | 'success' | 'warning' | 'error'
20
21
 
21
22
  /** A button rendered inside a toast (a primary `action` or a secondary `cancel`). */
@@ -27,6 +28,7 @@ export type ToastAction = {
27
28
  keepOpen?: boolean
28
29
  }
29
30
 
31
+ /** Options for one toast: its variant, how long it stays, and any action button. */
30
32
  export type ToastOptions = {
31
33
  variant?: ToastVariant
32
34
  /** Auto-dismiss after N ms. `0` = sticky (dismiss manually). Default 4000. */
@@ -43,6 +45,7 @@ export type ToastOptions = {
43
45
  render?: Snippet<[Toast]>
44
46
  }
45
47
 
48
+ /** A live toast: its options plus the id needed to dismiss it. */
46
49
  export type Toast = {
47
50
  id: number
48
51
  message: string
@@ -67,6 +70,7 @@ export type PromiseMessages<T> = {
67
70
 
68
71
  type VariantOptions = Omit<ToastOptions, 'variant'>
69
72
 
73
+ /** The callable toast API - `toast(msg)` plus `.success` / `.error` / friends. */
70
74
  export type ToastFn = {
71
75
  (message: string, options?: ToastOptions): number
72
76
  info: (message: string, options?: VariantOptions) => number
@@ -0,0 +1,207 @@
1
+ /**
2
+ * The dev-time config checks. The bar for each rule is two-sided: it must fire
3
+ * on the broken config AND stay silent on every valid shape near it, because a
4
+ * warning people learn to ignore is worse than no warning.
5
+ */
6
+ import { describe, expect, it } from 'vitest'
7
+ import { validateGridConfig } from './validate'
8
+
9
+ type Row = { name: string; amount: number }
10
+ const data: Row[] = [
11
+ { name: 'Ada', amount: 100 },
12
+ { name: 'Grace', amount: 200 },
13
+ ]
14
+
15
+ const run = (over: Partial<Parameters<typeof validateGridConfig>[0]> = {}) =>
16
+ validateGridConfig({
17
+ data,
18
+ columns: [{ field: 'name' }, { field: 'amount' }],
19
+ ...over,
20
+ } as any)
21
+
22
+ describe('unknown field', () => {
23
+ it('flags a field that is not on the data', () => {
24
+ const msgs = run({ columns: [{ field: 'naem' }, { field: 'amount' }] })
25
+ expect(msgs).toHaveLength(1)
26
+ expect(msgs[0]).toContain('"naem" does not exist')
27
+ })
28
+
29
+ it('suggests the near miss', () => {
30
+ expect(run({ columns: [{ field: 'naem' }] })[0]).toContain('Did you mean "name"')
31
+ })
32
+
33
+ it('does not guess when nothing is close', () => {
34
+ const msg = run({ columns: [{ field: 'zzzzzzzzz' }] })[0]!
35
+ expect(msg).toContain('does not exist')
36
+ expect(msg).not.toContain('Did you mean')
37
+ })
38
+
39
+ it('stays silent on valid fields', () => {
40
+ expect(run()).toEqual([])
41
+ })
42
+
43
+ it('ignores a computed column', () => {
44
+ expect(run({ columns: [{ id: 'full', fieldFn: (r: unknown) => (r as Row).name }] })).toEqual([])
45
+ })
46
+
47
+ it('ignores an id-only column, like an actions column', () => {
48
+ expect(run({ columns: [{ id: 'actions', header: '' }] })).toEqual([])
49
+ })
50
+
51
+ it('looks through group columns', () => {
52
+ const msgs = run({ columns: [{ header: 'Group', columns: [{ field: 'nope' }] }] })
53
+ expect(msgs[0]).toContain('"nope" does not exist')
54
+ })
55
+
56
+ it('tolerates sparse data - a key missing from the first row but present later', () => {
57
+ const sparse = [{ name: 'Ada' }, { name: 'Grace', amount: 200 }] as Row[]
58
+ expect(run({ data: sparse, columns: [{ field: 'amount' }] })).toEqual([])
59
+ })
60
+
61
+ it('says nothing when there is no data to check against', () => {
62
+ expect(run({ data: [], columns: [{ field: 'whatever' }] })).toEqual([])
63
+ })
64
+ })
65
+
66
+ describe('duplicate ids', () => {
67
+ it('flags two columns resolving to the same id', () => {
68
+ const msgs = run({ columns: [{ field: 'name' }, { field: 'name' }] })
69
+ expect(msgs.some((m) => m.includes('share the id "name"'))).toBe(true)
70
+ })
71
+
72
+ it('is happy when a duplicate field carries a distinct id', () => {
73
+ const msgs = run({
74
+ columns: [{ field: 'name' }, { id: 'name2', field: 'name' }],
75
+ })
76
+ expect(msgs.some((m) => m.includes('share the id'))).toBe(false)
77
+ })
78
+ })
79
+
80
+ describe('inert pageSize', () => {
81
+ it('flags pageSize when pagination was never mentioned', () => {
82
+ expect(run({ pageSize: 25 })[0]).toContain('pagination was never turned on')
83
+ })
84
+
85
+ it('is silent with pageable', () => {
86
+ expect(run({ pageSize: 25, pageable: true })).toEqual([])
87
+ })
88
+
89
+ it('is silent with showPagination', () => {
90
+ expect(run({ pageSize: 25, showPagination: true })).toEqual([])
91
+ })
92
+
93
+ it('is silent when pagination is deliberately off', () => {
94
+ // A bound toggle starting false - our own shortcut-config demo does this,
95
+ // and warning on it was noise, not a finding.
96
+ expect(run({ pageSize: 25, pageable: false })).toEqual([])
97
+ expect(run({ pageSize: 25, showPagination: false })).toEqual([])
98
+ })
99
+ })
100
+
101
+ describe('column sortable without sorting', () => {
102
+ const columns = [{ field: 'name', sortable: true }]
103
+
104
+ it('flags it when nothing enables sorting', () => {
105
+ expect(run({ columns })[0]).toContain('sorting is not enabled')
106
+ })
107
+
108
+ it('is silent when the grid shortcut is on', () => {
109
+ expect(run({ columns, sortable: true })).toEqual([])
110
+ })
111
+
112
+ it('is silent when the feature is registered explicitly', () => {
113
+ expect(run({ columns, features: { rowSortingFeature: {} } })).toEqual([])
114
+ })
115
+ })
116
+
117
+ describe('column ids referenced by other props', () => {
118
+ it('flags a groupBy naming a column that does not exist', () => {
119
+ const msg = run({ groupBy: ['naem'] })[0]!
120
+ expect(msg).toContain('`groupBy` refers to column "naem"')
121
+ expect(msg).toContain('Did you mean "name"')
122
+ })
123
+
124
+ it('is silent for a groupBy that matches', () => {
125
+ expect(run({ groupBy: ['name'] })).toEqual([])
126
+ })
127
+
128
+ it('flags a treeData.column that does not exist', () => {
129
+ expect(run({ treeData: { parentField: 'name', column: 'nope' } })[0]).toContain(
130
+ '`treeData.column` refers to column "nope"',
131
+ )
132
+ })
133
+ })
134
+
135
+ describe('treeData fields', () => {
136
+ it('flags a parentField that is not on the data', () => {
137
+ const msg = run({ treeData: { parentField: 'managerId' } })[0]!
138
+ expect(msg).toContain('`treeData.parentField` is "managerId"')
139
+ expect(msg).toContain('every row becomes a root')
140
+ })
141
+
142
+ it('flags an explicit idField that is not on the data', () => {
143
+ const msgs = run({ treeData: { parentField: 'name', idField: 'uid' } })
144
+ expect(msgs.some((m) => m.includes('`treeData.idField` is "uid"'))).toBe(true)
145
+ })
146
+
147
+ it('does not complain about the default idField', () => {
148
+ // `idField` defaults to 'id', which this fixture does not have - guessing
149
+ // about a default the user never wrote would be a false positive.
150
+ expect(run({ treeData: { parentField: 'name' } })).toEqual([])
151
+ })
152
+ })
153
+
154
+ describe('pinning versus column virtualization', () => {
155
+ it('flags pinning while column virtualization is on by default', () => {
156
+ const msg = run({ initialColumnPinning: { left: ['name'] } })[0]!
157
+ expect(msg).toContain('will not stick')
158
+ expect(msg).toContain('columnVirtualization={false}')
159
+ })
160
+
161
+ it('is silent once column virtualization is off', () => {
162
+ expect(
163
+ run({ initialColumnPinning: { left: ['name'] }, columnVirtualization: false }),
164
+ ).toEqual([])
165
+ })
166
+
167
+ it('is silent when nothing is pinned', () => {
168
+ expect(run({ initialColumnPinning: { left: [] } })).toEqual([])
169
+ })
170
+ })
171
+
172
+ describe('server-mode contracts', () => {
173
+ it('flags externalPagination without rowCount', () => {
174
+ expect(run({ externalPagination: true })[0]).toContain('`rowCount` is not set')
175
+ })
176
+
177
+ it('is silent when rowCount is supplied', () => {
178
+ expect(run({ externalPagination: true, rowCount: 500 })).toEqual([])
179
+ })
180
+
181
+ it('flags externalSort with no handler', () => {
182
+ expect(run({ externalSort: true })[0]).toContain('no `onSortingChange` handler')
183
+ })
184
+
185
+ it('is silent when the sort handler is wired', () => {
186
+ expect(run({ externalSort: true, onSortingChange: () => {} })).toEqual([])
187
+ })
188
+
189
+ it('flags externalFilter with no handler', () => {
190
+ expect(run({ externalFilter: true })[0]).toContain('no `onFiltersChange` handler')
191
+ })
192
+
193
+ it('is silent when the filter handler is wired', () => {
194
+ expect(run({ externalFilter: true, onFiltersChange: () => {} })).toEqual([])
195
+ })
196
+ })
197
+
198
+ describe('cost', () => {
199
+ it('samples the data rather than scanning it', () => {
200
+ // 100k rows must not cost 100k key reads: the check is capped at a fixed
201
+ // sample, so this returns immediately.
202
+ const many = Array.from({ length: 100_000 }, (_, i) => ({ name: 'n' + i, amount: i }))
203
+ const started = Date.now()
204
+ expect(run({ data: many })).toEqual([])
205
+ expect(Date.now() - started).toBeLessThan(200)
206
+ })
207
+ })