@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,269 @@
1
+ /**
2
+ * Development-time configuration checks.
3
+ *
4
+ * The grid used to fail silently on the most common mistakes. A misspelled
5
+ * `field` rendered a column of empty cells with nothing on the console; a
6
+ * `pageSize` with no pagination was quietly ignored; a column marked
7
+ * `sortable` with no sorting enabled just did not sort. Each of those costs
8
+ * someone an afternoon, and none of them were detectable from the outside.
9
+ *
10
+ * This module is the check. It is pure and returns messages rather than
11
+ * printing them, so it can be unit-tested; the controller runs it in an effect
12
+ * and prints each message once, in dev builds only.
13
+ *
14
+ * Rules for anything added here:
15
+ * - Never fire on valid config. A false positive teaches people to ignore
16
+ * warnings, which is worse than staying silent.
17
+ * - Say what happened, what it means, and how to fix it - the house style set
18
+ * by the one pre-existing warning in server-data-source.ts.
19
+ * - Stay O(columns), or O(columns x a fixed row sample). This runs on data
20
+ * changes, and must not scale with row count.
21
+ */
22
+ import type { ColumnDef, RowData, TableFeatures } from './core'
23
+
24
+ /** How many rows to sample when deciding whether a `field` exists. */
25
+ const FIELD_SAMPLE_ROWS = 10
26
+
27
+ const DOCS = 'https://svgrid.com/docs/getting-started/3-data-and-columns/'
28
+
29
+ export type ValidateInput<TFeatures extends TableFeatures, TData extends RowData> = {
30
+ data: ReadonlyArray<TData>
31
+ columns: ReadonlyArray<ColumnDef<TFeatures, TData>>
32
+ /** The resolved feature set, after the boolean shortcuts have injected theirs. */
33
+ features?: Record<string, unknown>
34
+ sortable?: boolean
35
+ pageable?: boolean
36
+ showPagination?: boolean
37
+ pageSize?: number
38
+ groupBy?: ReadonlyArray<string>
39
+ treeData?: { parentField?: string; idField?: string; column?: string }
40
+ initialColumnPinning?: { left?: ReadonlyArray<string>; right?: ReadonlyArray<string> }
41
+ columnVirtualization?: boolean
42
+ externalPagination?: boolean
43
+ rowCount?: number
44
+ externalSort?: boolean
45
+ onSortingChange?: unknown
46
+ externalFilter?: boolean
47
+ onFiltersChange?: unknown
48
+ }
49
+
50
+ /** Every leaf column, flattened through group columns. */
51
+ function leaves<TFeatures extends TableFeatures, TData extends RowData>(
52
+ columns: ReadonlyArray<ColumnDef<TFeatures, TData>>,
53
+ out: Array<ColumnDef<TFeatures, TData>> = [],
54
+ ): Array<ColumnDef<TFeatures, TData>> {
55
+ for (const col of columns) {
56
+ if (col.columns?.length) leaves(col.columns, out)
57
+ else out.push(col)
58
+ }
59
+ return out
60
+ }
61
+
62
+ export function validateGridConfig<
63
+ TFeatures extends TableFeatures,
64
+ TData extends RowData,
65
+ >(input: ValidateInput<TFeatures, TData>): string[] {
66
+ const messages: string[] = []
67
+ const cols = leaves(input.columns ?? [])
68
+
69
+ // ---- 1. A `field` that does not exist on the data -------------------------
70
+ // The single most expensive silent failure: the column renders, and every
71
+ // 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
+ const known = new Set<string>()
76
+ for (const row of sample) {
77
+ if (row && typeof row === 'object') for (const k of Object.keys(row)) known.add(k)
78
+ }
79
+ for (const col of cols) {
80
+ const field = col.field as string | undefined
81
+ // `fieldFn` computes its value, and an id-only column (actions, buttons)
82
+ // never reads the row - neither needs a matching key.
83
+ if (!field || col.fieldFn) continue
84
+ if (known.has(field)) continue
85
+ const guess = nearest(field, [...known])
86
+ messages.push(
87
+ `[svgrid] Column field "${field}" does not exist on your row data, so that ` +
88
+ `column renders empty.${guess ? ` Did you mean "${guess}"?` : ''} ` +
89
+ `Available keys: ${[...known].slice(0, 12).join(', ')}. See ${DOCS}`,
90
+ )
91
+ }
92
+ }
93
+
94
+ // ---- 2. Duplicate column ids ---------------------------------------------
95
+ // Two columns resolving to the same id makes selection, sorting and column
96
+ // state address the wrong one, with no error anywhere.
97
+ const seen = new Map<string, number>()
98
+ for (const col of cols) {
99
+ const id = (col.id ?? (col.field as string | undefined)) as string | undefined
100
+ if (!id) continue
101
+ seen.set(id, (seen.get(id) ?? 0) + 1)
102
+ }
103
+ for (const [id, n] of seen) {
104
+ if (n > 1) {
105
+ messages.push(
106
+ `[svgrid] ${n} columns share the id "${id}". Column state (sorting, ` +
107
+ `filtering, pinning, width) is keyed by id, so they will act as one. ` +
108
+ `Give each an explicit \`id\`. See ${DOCS}`,
109
+ )
110
+ }
111
+ }
112
+
113
+ // ---- 3. `pageSize` with pagination never asked for ------------------------
114
+ // Only when BOTH pagination props are absent. `pageable={false}` is a
115
+ // deliberate statement - often a bound toggle that flips on later - whereas
116
+ // an unset prop means the author never considered it. Warning on the former
117
+ // fired on our own shortcut-config demo, which is exactly the kind of noise
118
+ // that teaches people to ignore warnings.
119
+ const paginationUnset =
120
+ input.pageable === undefined && input.showPagination === undefined
121
+ if (input.pageSize !== undefined && paginationUnset) {
122
+ messages.push(
123
+ '[svgrid] `pageSize` is set but pagination was never turned on, so it has ' +
124
+ 'no effect and every row renders. Add `pageable` to switch the pager on.',
125
+ )
126
+ }
127
+
128
+ // ---- 4. A column asks to sort, but nothing enables sorting ----------------
129
+ // `sortable` on the grid injects rowSortingFeature, so only warn when neither
130
+ // the shortcut nor an explicitly registered feature is present.
131
+ const sortingEnabled =
132
+ input.sortable === true || Boolean(input.features?.rowSortingFeature)
133
+ if (!sortingEnabled && cols.some((c) => c.sortable === true)) {
134
+ messages.push(
135
+ '[svgrid] A column sets `sortable: true`, but sorting is not enabled on the ' +
136
+ 'grid, so its header does nothing. Add `sortable` to <SvGrid> (it registers ' +
137
+ 'the sorting feature for you).',
138
+ )
139
+ }
140
+
141
+ // ---- 5. Column ids referenced by other props ------------------------------
142
+ // `groupBy` and `treeData.column` address columns by id. A name that matches
143
+ // nothing is silently ignored, so grouping or the tree expander just never
144
+ // appears and there is nothing to debug against.
145
+ const columnIds = new Set<string>()
146
+ for (const col of cols) {
147
+ const id = (col.id ?? (col.field as string | undefined)) as string | undefined
148
+ if (id) columnIds.add(id)
149
+ }
150
+ for (const id of input.groupBy ?? []) {
151
+ if (columnIds.has(id)) continue
152
+ const guess = nearest(id, [...columnIds])
153
+ messages.push(
154
+ `[svgrid] \`groupBy\` refers to column "${id}", which does not exist, so ` +
155
+ `that grouping level is ignored.${guess ? ` Did you mean "${guess}"?` : ''} ` +
156
+ `Column ids: ${[...columnIds].slice(0, 12).join(', ')}`,
157
+ )
158
+ }
159
+ const treeColumn = input.treeData?.column
160
+ if (treeColumn && !columnIds.has(treeColumn)) {
161
+ messages.push(
162
+ `[svgrid] \`treeData.column\` refers to column "${treeColumn}", which does ` +
163
+ `not exist, so the expander falls back to the first visible column.`,
164
+ )
165
+ }
166
+
167
+ // ---- 6. treeData pointing at fields the data does not have ----------------
168
+ if (sample.length && input.treeData) {
169
+ const known = new Set<string>()
170
+ for (const row of sample) {
171
+ if (row && typeof row === 'object') for (const k of Object.keys(row)) known.add(k)
172
+ }
173
+ const parentField = input.treeData.parentField
174
+ if (parentField && !known.has(parentField)) {
175
+ const guess = nearest(parentField, [...known])
176
+ messages.push(
177
+ `[svgrid] \`treeData.parentField\` is "${parentField}", which is not on ` +
178
+ `your row data, so every row becomes a root and no hierarchy appears.` +
179
+ `${guess ? ` Did you mean "${guess}"?` : ''}`,
180
+ )
181
+ }
182
+ // `idField` defaults to 'id'; only check what was asked for explicitly.
183
+ const idField = input.treeData.idField
184
+ if (idField && !known.has(idField)) {
185
+ messages.push(
186
+ `[svgrid] \`treeData.idField\` is "${idField}", which is not on your row ` +
187
+ `data, so parent lookups cannot match and the tree stays flat.`,
188
+ )
189
+ }
190
+ }
191
+
192
+ // ---- 7. Pinning that column virtualization will hide ----------------------
193
+ // Documented incompatibility: the virtualizer recycles column DOM nodes, so
194
+ // sticky pinning cannot survive it. `columnVirtualization` defaults to ON,
195
+ // which means the natural way to write this silently does nothing.
196
+ const pinned =
197
+ (input.initialColumnPinning?.left?.length ?? 0) +
198
+ (input.initialColumnPinning?.right?.length ?? 0)
199
+ if (pinned > 0 && input.columnVirtualization !== false) {
200
+ messages.push(
201
+ '[svgrid] `initialColumnPinning` is set while column virtualization is on ' +
202
+ '(its default), so the pinned columns will not stick - the virtualizer ' +
203
+ 'recycles column nodes. Add `columnVirtualization={false}`.',
204
+ )
205
+ }
206
+
207
+ // ---- 8. Server-mode contracts left half-wired -----------------------------
208
+ // Each of these makes the grid hand control to the consumer. Miss the other
209
+ // half and the feature looks broken rather than unconfigured.
210
+ if (input.externalPagination === true && input.rowCount === undefined) {
211
+ messages.push(
212
+ '[svgrid] `externalPagination` is on but `rowCount` is not set, so the pager ' +
213
+ 'cannot know how many pages exist. Pass the server total as `rowCount`.',
214
+ )
215
+ }
216
+ if (input.externalSort === true && !input.onSortingChange) {
217
+ messages.push(
218
+ '[svgrid] `externalSort` is on but there is no `onSortingChange` handler, so ' +
219
+ 'clicking a header changes nothing - the grid stopped sorting and nobody ' +
220
+ 'is listening. Add `onSortingChange` and re-fetch in your handler.',
221
+ )
222
+ }
223
+ if (input.externalFilter === true && !input.onFiltersChange) {
224
+ messages.push(
225
+ '[svgrid] `externalFilter` is on but there is no `onFiltersChange` handler, ' +
226
+ 'so filtering the grid changes nothing. Add `onFiltersChange` and re-fetch ' +
227
+ 'in your handler.',
228
+ )
229
+ }
230
+
231
+ return messages
232
+ }
233
+
234
+ /** Closest key by edit distance, for a "did you mean" hint. Undefined if none is close. */
235
+ function nearest(target: string, candidates: string[]): string | undefined {
236
+ let best: string | undefined
237
+ let bestScore = Infinity
238
+ for (const c of candidates) {
239
+ const d = distance(target.toLowerCase(), c.toLowerCase())
240
+ if (d < bestScore) {
241
+ bestScore = d
242
+ best = c
243
+ }
244
+ }
245
+ // Only suggest a genuinely near miss - a third of the length, at most 3 edits.
246
+ const limit = Math.min(3, Math.floor(target.length / 3) + 1)
247
+ return bestScore <= limit ? best : undefined
248
+ }
249
+
250
+ /** Levenshtein distance, iterative single-row. Inputs here are identifier-length. */
251
+ function distance(a: string, b: string): number {
252
+ if (a === b) return 0
253
+ const prev = new Array<number>(b.length + 1)
254
+ for (let j = 0; j <= b.length; j++) prev[j] = j
255
+ for (let i = 1; i <= a.length; i++) {
256
+ let carry = prev[0]!
257
+ prev[0] = i
258
+ for (let j = 1; j <= b.length; j++) {
259
+ const next = Math.min(
260
+ prev[j]! + 1,
261
+ prev[j - 1]! + 1,
262
+ carry + (a[i - 1] === b[j - 1] ? 0 : 1),
263
+ )
264
+ carry = prev[j]!
265
+ prev[j] = next
266
+ }
267
+ }
268
+ return prev[b.length]!
269
+ }