@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.
- package/dist/SvGrid.controller.svelte.d.ts +1 -0
- package/dist/SvGrid.controller.svelte.js +64 -2
- package/dist/SvGrid.svelte +2 -1
- package/dist/SvGrid.types.d.ts +150 -0
- package/dist/ai.d.ts +28 -0
- package/dist/ai.js +6 -0
- package/dist/cdn/{GridMenus-B0F9iBrG.js → GridMenus-BfTAKn84.js} +1 -1
- package/dist/cdn/{GridMenus-IHK_l7m6.js → GridMenus-C3bJd7w8.js} +1 -1
- package/dist/cdn/{src-Cd0tearp.js → src-BYq-qyrp.js} +1012 -999
- package/dist/cdn/{src-B1TdiyS8.js → src-DBel9wRZ.js} +1324 -1311
- package/dist/cdn/svgrid.js +1 -1
- package/dist/cdn/svgrid.svelte-external.js +1 -1
- package/dist/cdn/validate-_CDJzgIo.js +75 -0
- package/dist/cell-formatting.d.ts +2 -0
- package/dist/cell-formatting.js +2 -0
- package/dist/chart-export.d.ts +1 -0
- package/dist/chart.d.ts +31 -5
- package/dist/chart.js +9 -3
- package/dist/core.d.ts +197 -0
- package/dist/core.js +72 -0
- package/dist/createTree.svelte.d.ts +3 -0
- package/dist/createTree.svelte.js +1 -0
- package/dist/datetime/date-core.d.ts +2 -0
- package/dist/datetime/date-restrict.d.ts +1 -0
- package/dist/datetime/timezone.d.ts +1 -0
- package/dist/dock-manager-model.d.ts +3 -0
- package/dist/dock-manager-model.js +1 -0
- package/dist/dock-model.d.ts +6 -0
- package/dist/dock-model.js +3 -0
- package/dist/editor-contract.d.ts +1 -0
- package/dist/list-option.d.ts +1 -0
- package/dist/positioning.d.ts +2 -0
- package/dist/scheduler-ical.d.ts +1 -0
- package/dist/scheduler-model.d.ts +1 -0
- package/dist/summaries.js +22 -0
- package/dist/svgrid-wrapper.types.d.ts +5 -0
- package/dist/toast-store.svelte.d.ts +4 -0
- package/dist/validate.d.ts +50 -0
- package/dist/validate.js +187 -0
- package/package.json +4 -1
- package/src/SvGrid.controller.svelte.ts +68 -2
- package/src/SvGrid.svelte +2 -1
- package/src/SvGrid.types.ts +150 -0
- package/src/ai.ts +28 -0
- package/src/cell-formatting.ts +2 -0
- package/src/chart-export.ts +1 -0
- package/src/chart.ts +31 -5
- package/src/core.ts +207 -0
- package/src/createTree.svelte.ts +3 -0
- package/src/datetime/date-core.ts +2 -0
- package/src/datetime/date-restrict.ts +1 -0
- package/src/datetime/timezone.ts +1 -0
- package/src/dock-manager-model.ts +3 -0
- package/src/dock-model.ts +6 -0
- package/src/editor-contract.ts +1 -0
- package/src/list-option.ts +1 -0
- package/src/positioning.ts +2 -0
- package/src/scheduler-ical.ts +1 -0
- package/src/scheduler-model.ts +1 -0
- package/src/summaries.ts +21 -0
- package/src/svgrid-wrapper.types.ts +5 -0
- package/src/svgrid.summaries.test.ts +217 -0
- package/src/toast-store.svelte.ts +4 -0
- package/src/validate.test.ts +207 -0
- package/src/validate.ts +269 -0
package/src/validate.ts
ADDED
|
@@ -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
|
+
}
|