@svgrid/grid 2.6.3 → 2.6.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.
- package/dist/SvGrid.controller.svelte.d.ts +2 -1
- package/dist/SvGrid.controller.svelte.js +145 -3
- package/dist/SvGrid.css +32 -0
- package/dist/SvGrid.svelte +64 -6
- package/dist/SvGrid.types.d.ts +61 -0
- package/dist/a11y/grid-announcements.d.ts +58 -0
- package/dist/a11y/grid-announcements.js +70 -0
- package/dist/advanced-filter.svelte.d.ts +42 -0
- package/dist/advanced-filter.svelte.js +13 -0
- package/dist/build-api.js +44 -2
- package/dist/cdn/{GridMenus-BoVKgAv8.js → GridMenus-CBvjL5u4.js} +14 -14
- package/dist/cdn/{GridMenus-DL1zrDwf.js → GridMenus-DAzp6YXc.js} +14 -14
- package/dist/cdn/{src-M2NfKnjX.js → src-3WKN7L4G.js} +5932 -5743
- package/dist/cdn/{src-CEpw6Ato.js → src-Cl7h5-Ux.js} +3371 -3182
- package/dist/cdn/svgrid.js +8 -8
- package/dist/cdn/svgrid.svelte-external.js +8 -8
- package/dist/filter-operators.d.ts +2 -1
- package/dist/filter-operators.js +5 -6
- package/dist/filtering/excel-filters.d.ts +1 -0
- package/dist/filtering/excel-filters.js +5 -0
- package/dist/filtering/filter-operator-catalogue.d.ts +42 -0
- package/dist/filtering/filter-operator-catalogue.js +42 -0
- package/dist/filtering/predicate-expr.d.ts +78 -0
- package/dist/filtering/predicate-expr.js +1 -0
- package/dist/filtering/row-predicate.d.ts +17 -0
- package/dist/filtering/row-predicate.js +55 -0
- package/dist/grid-messages.d.ts +7 -0
- package/dist/grid-messages.js +7 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/server-data-source.d.ts +34 -0
- package/dist/server-data-source.js +19 -0
- package/dist/svgrid-wrapper.types.d.ts +37 -5
- package/dist/themes/index.js +12 -4
- package/package.json +2 -1
- package/src/SvGrid.controller.svelte.ts +169 -2
- package/src/SvGrid.css +32 -0
- package/src/SvGrid.svelte +64 -6
- package/src/SvGrid.types.ts +61 -0
- package/src/a11y/grid-announcements.test.ts +78 -0
- package/src/a11y/grid-announcements.ts +94 -0
- package/src/a11y.announce.test.ts +250 -0
- package/src/a11y.axe.test.ts +194 -0
- package/src/advanced-filter.svelte.ts +59 -0
- package/src/build-api.ts +43 -2
- package/src/filter-operators.ts +9 -6
- package/src/filtering/excel-filters.ts +15 -0
- package/src/filtering/filter-operator-catalogue.ts +74 -0
- package/src/filtering/node-entry.test.ts +75 -0
- package/src/filtering/predicate-expr.ts +50 -0
- package/src/filtering/row-predicate.ts +57 -0
- package/src/grid-messages.ts +18 -0
- package/src/index.ts +28 -0
- package/src/server-data-source.test.ts +94 -0
- package/src/server-data-source.ts +56 -0
- package/src/svgrid-wrapper.types.ts +37 -5
- package/src/svgrid.advanced-filter.test.ts +481 -0
- package/src/svgrid.displayed-rows.test.ts +144 -0
- package/src/svgrid.group-props-reactive.svelte.test.ts +151 -0
- package/src/svgrid.group-props.test.ts +280 -0
- package/src/themes/contrast.test.ts +112 -0
- package/src/themes/index.ts +12 -4
- package/themes/catppuccin.css +3 -3
- package/themes/dracula.css +3 -3
- package/themes/nord.css +3 -3
- package/themes/notion.css +4 -4
package/src/grid-messages.ts
CHANGED
|
@@ -39,6 +39,17 @@ export type GridMessages = {
|
|
|
39
39
|
total: string
|
|
40
40
|
rowsSuffix: string
|
|
41
41
|
rowSuffix: string
|
|
42
|
+
// Screen-reader announcements. Unlike the labels above these are whole
|
|
43
|
+
// sentences, so they take `{placeholders}` - a translator needs to control
|
|
44
|
+
// word order, which assembling atoms in English order does not allow.
|
|
45
|
+
announceFilterResults: string
|
|
46
|
+
announceNoMatches: string
|
|
47
|
+
announceFiltersCleared: string
|
|
48
|
+
announceRowsSelected: string
|
|
49
|
+
announceSelectionCleared: string
|
|
50
|
+
// Advanced-filter toolbar chip
|
|
51
|
+
advancedFilterActive: string
|
|
52
|
+
advancedFilterClear: string
|
|
42
53
|
// Filter operator labels (mirror filterOperatorOptions ids)
|
|
43
54
|
opContains: string
|
|
44
55
|
opNotContains: string
|
|
@@ -97,6 +108,13 @@ export const defaultGridMessages: GridMessages = {
|
|
|
97
108
|
total: 'Total',
|
|
98
109
|
rowsSuffix: 'rows',
|
|
99
110
|
rowSuffix: 'row',
|
|
111
|
+
announceFilterResults: '{visible} of {total} rows match the current filters',
|
|
112
|
+
announceNoMatches: 'No rows match the current filters',
|
|
113
|
+
announceFiltersCleared: 'Filters cleared, showing all {total} rows',
|
|
114
|
+
announceRowsSelected: '{count} rows selected',
|
|
115
|
+
announceSelectionCleared: 'Selection cleared',
|
|
116
|
+
advancedFilterActive: 'Advanced filter',
|
|
117
|
+
advancedFilterClear: 'Clear advanced filter',
|
|
100
118
|
menuCopy: 'Copy',
|
|
101
119
|
menuCut: 'Cut',
|
|
102
120
|
menuPaste: 'Paste',
|
package/src/index.ts
CHANGED
|
@@ -709,6 +709,34 @@ export {
|
|
|
709
709
|
type ExcelFilterOperator,
|
|
710
710
|
type ExcelFilterOptions,
|
|
711
711
|
} from './filtering/excel-filters'
|
|
712
|
+
export {
|
|
713
|
+
ALL_FILTER_OPERATORS,
|
|
714
|
+
SET_OPERATOR_IDS,
|
|
715
|
+
VALUELESS_OPERATOR_IDS,
|
|
716
|
+
RANGE_OPERATOR_IDS,
|
|
717
|
+
isSetOperator,
|
|
718
|
+
isValuelessOperator,
|
|
719
|
+
isRangeOperator,
|
|
720
|
+
type FilterValueType,
|
|
721
|
+
} from './filtering/filter-operator-catalogue'
|
|
722
|
+
export { applyRowPredicate } from './filtering/row-predicate'
|
|
723
|
+
export type {
|
|
724
|
+
GridPredicateExpr,
|
|
725
|
+
GridScalarExpr,
|
|
726
|
+
GridComparisonOp,
|
|
727
|
+
GridArithmeticOp,
|
|
728
|
+
GridAggFn,
|
|
729
|
+
} from './filtering/predicate-expr'
|
|
730
|
+
// Advanced-filter seam: enterprise registers the compiler, the grid owns the
|
|
731
|
+
// state and the pipeline slot.
|
|
732
|
+
export {
|
|
733
|
+
registerAdvancedFilterEngine,
|
|
734
|
+
getAdvancedFilterEngine,
|
|
735
|
+
hasAdvancedFilterEngine,
|
|
736
|
+
type AdvancedFilterEngine,
|
|
737
|
+
type AdvancedFilterCompileContext,
|
|
738
|
+
type CompiledRowPredicate,
|
|
739
|
+
} from './advanced-filter.svelte'
|
|
712
740
|
export {
|
|
713
741
|
getGridCellDomId,
|
|
714
742
|
getGridCellA11yProps,
|
|
@@ -287,3 +287,97 @@ describe('createServerDataSource optimistic mutations', () => {
|
|
|
287
287
|
expect(src.getRows).toHaveBeenCalledTimes(2)
|
|
288
288
|
})
|
|
289
289
|
})
|
|
290
|
+
|
|
291
|
+
describe('advanced-filter expression contract', () => {
|
|
292
|
+
const EXPR = { kind: 'cmp', column: 'id', op: 'greaterThan', value: '5' } as never
|
|
293
|
+
|
|
294
|
+
/** A source that reports whether it honoured the expression. */
|
|
295
|
+
function exprSource(ack: boolean | undefined) {
|
|
296
|
+
const seen: unknown[] = []
|
|
297
|
+
const source: ServerDataSource<Row> = {
|
|
298
|
+
async getRows(req) {
|
|
299
|
+
seen.push(req.filterModel.expression)
|
|
300
|
+
return {
|
|
301
|
+
rows: [{ id: 1 }],
|
|
302
|
+
rowCount: 1,
|
|
303
|
+
...(ack === undefined ? {} : { appliedExpression: ack }),
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
return { source, seen }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
it('forwards the expression to the source', async () => {
|
|
311
|
+
const { source, seen } = exprSource(true)
|
|
312
|
+
const ctl = createServerDataSource(source, { pageSize: 10, onChange: () => {} })
|
|
313
|
+
ctl.setFilter({ expression: EXPR })
|
|
314
|
+
await flush()
|
|
315
|
+
expect(seen.at(-1)).toEqual(EXPR)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('is satisfied when the source acknowledges applying it', async () => {
|
|
319
|
+
const ctl = createServerDataSource(exprSource(true).source, { pageSize: 10, onChange: () => {} })
|
|
320
|
+
ctl.setFilter({ expression: EXPR })
|
|
321
|
+
await flush()
|
|
322
|
+
expect(ctl.getState().expressionUnapplied).toBe(false)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('flags an unapplied expression when the ack is missing', async () => {
|
|
326
|
+
const ctl = createServerDataSource(exprSource(undefined).source, { pageSize: 10, onChange: () => {} })
|
|
327
|
+
ctl.setFilter({ expression: EXPR })
|
|
328
|
+
await flush()
|
|
329
|
+
// The rows look perfectly normal, which is exactly why this has to be
|
|
330
|
+
// surfaced rather than inferred by the user.
|
|
331
|
+
expect(ctl.getState().expressionUnapplied).toBe(true)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('flags it when the source explicitly declines', async () => {
|
|
335
|
+
const ctl = createServerDataSource(exprSource(false).source, { pageSize: 10, onChange: () => {} })
|
|
336
|
+
ctl.setFilter({ expression: EXPR })
|
|
337
|
+
await flush()
|
|
338
|
+
expect(ctl.getState().expressionUnapplied).toBe(true)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('does NOT client-filter the loaded page as a fallback', async () => {
|
|
342
|
+
const ctl = createServerDataSource(exprSource(undefined).source, { pageSize: 10, onChange: () => {} })
|
|
343
|
+
ctl.setFilter({ expression: EXPR })
|
|
344
|
+
await flush()
|
|
345
|
+
// id 1 fails `id > 5`. Filtering here would turn "1 of 1,000,000 match"
|
|
346
|
+
// into a confident lie and make paging incoherent, so the row survives and
|
|
347
|
+
// the flag carries the truth instead.
|
|
348
|
+
expect(ctl.getState().rows).toEqual([{ id: 1 }])
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('warns once, not on every fetch', async () => {
|
|
352
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
353
|
+
try {
|
|
354
|
+
const ctl = createServerDataSource(exprSource(undefined).source, { pageSize: 10, onChange: () => {} })
|
|
355
|
+
ctl.setFilter({ expression: EXPR })
|
|
356
|
+
await flush()
|
|
357
|
+
ctl.refresh()
|
|
358
|
+
await flush()
|
|
359
|
+
ctl.refresh()
|
|
360
|
+
await flush()
|
|
361
|
+
expect(warn).toHaveBeenCalledTimes(1)
|
|
362
|
+
} finally {
|
|
363
|
+
warn.mockRestore()
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('stays clear when no expression is in play', async () => {
|
|
368
|
+
const ctl = createServerDataSource(exprSource(undefined).source, { pageSize: 10, onChange: () => {} })
|
|
369
|
+
ctl.refresh()
|
|
370
|
+
await flush()
|
|
371
|
+
expect(ctl.getState().expressionUnapplied).toBe(false)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('clears the flag once the expression is removed', async () => {
|
|
375
|
+
const ctl = createServerDataSource(exprSource(undefined).source, { pageSize: 10, onChange: () => {} })
|
|
376
|
+
ctl.setFilter({ expression: EXPR })
|
|
377
|
+
await flush()
|
|
378
|
+
expect(ctl.getState().expressionUnapplied).toBe(true)
|
|
379
|
+
ctl.setFilter({})
|
|
380
|
+
await flush()
|
|
381
|
+
expect(ctl.getState().expressionUnapplied).toBe(false)
|
|
382
|
+
})
|
|
383
|
+
})
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* non-optimistic for now - the grid reflects a change only after the
|
|
16
16
|
* follow-up re-fetch of the current page lands.
|
|
17
17
|
*/
|
|
18
|
+
import type { GridPredicateExpr } from './filtering/predicate-expr'
|
|
19
|
+
|
|
18
20
|
export type ServerSortModel = Array<{ id: string; desc: boolean }>
|
|
19
21
|
|
|
20
22
|
export type ServerFilterModel = {
|
|
@@ -29,6 +31,24 @@ export type ServerFilterModel = {
|
|
|
29
31
|
string,
|
|
30
32
|
{ operator: string; value: string; valueTo?: string; selectedValues?: string[] }
|
|
31
33
|
>
|
|
34
|
+
/**
|
|
35
|
+
* Advanced-filter predicate (Pro), as a JSON AST. Expresses what `columns`
|
|
36
|
+
* cannot: OR across columns, nesting, negation, two conditions on one
|
|
37
|
+
* column, cross-column comparison, and aggregates.
|
|
38
|
+
*
|
|
39
|
+
* CONTRACT - all or nothing. A backend that receives this MUST either:
|
|
40
|
+
*
|
|
41
|
+
* (a) translate the WHOLE expression into its query, make `rowCount`
|
|
42
|
+
* reflect it, and set `appliedExpression: true` on the result; or
|
|
43
|
+
* (b) apply none of it and leave `appliedExpression` unset.
|
|
44
|
+
*
|
|
45
|
+
* Partial application is a contract violation, not a degraded mode: it
|
|
46
|
+
* returns a SUPERSET of the requested rows while the UI says the filter is
|
|
47
|
+
* on. That is strictly worse than not filtering, because nothing about the
|
|
48
|
+
* result looks wrong. When the ack is missing the grid says so rather than
|
|
49
|
+
* filtering the loaded page itself - see `ServerState.expressionUnapplied`.
|
|
50
|
+
*/
|
|
51
|
+
expression?: GridPredicateExpr
|
|
32
52
|
}
|
|
33
53
|
|
|
34
54
|
/** A value column to roll up per group. */
|
|
@@ -144,6 +164,12 @@ export type ServerResult<TData> = {
|
|
|
144
164
|
rows: ReadonlyArray<TData>
|
|
145
165
|
/** Total row count after filtering (for the pager). */
|
|
146
166
|
rowCount: number
|
|
167
|
+
/**
|
|
168
|
+
* Set `true` ONLY when `filterModel.expression` was applied in full. Leave it
|
|
169
|
+
* unset if you ignored the expression; the grid then warns rather than
|
|
170
|
+
* pretending the filter ran. See the contract on `ServerFilterModel.expression`.
|
|
171
|
+
*/
|
|
172
|
+
appliedExpression?: boolean
|
|
147
173
|
}
|
|
148
174
|
|
|
149
175
|
export type ServerDataSource<TData> = {
|
|
@@ -171,6 +197,15 @@ export type ServerState<TData> = {
|
|
|
171
197
|
pageCount: number
|
|
172
198
|
sortModel: ServerSortModel
|
|
173
199
|
filterModel: ServerFilterModel
|
|
200
|
+
/**
|
|
201
|
+
* True when an advanced-filter expression was sent but the source did not
|
|
202
|
+
* acknowledge applying it - so `rows` is unfiltered and the UI should say so.
|
|
203
|
+
* Surface this rather than hiding it: the rows look perfectly normal.
|
|
204
|
+
*
|
|
205
|
+
* Optional so existing code that builds a `ServerState` literal keeps
|
|
206
|
+
* compiling; the controller always sets it.
|
|
207
|
+
*/
|
|
208
|
+
expressionUnapplied?: boolean
|
|
174
209
|
}
|
|
175
210
|
|
|
176
211
|
export type ServerController<TData> = {
|
|
@@ -232,12 +267,16 @@ export function createServerDataSource<TData>(
|
|
|
232
267
|
pageCount: 1,
|
|
233
268
|
sortModel: [],
|
|
234
269
|
filterModel: {},
|
|
270
|
+
expressionUnapplied: false,
|
|
235
271
|
}
|
|
236
272
|
|
|
237
273
|
// Monotonic request id: only the latest fetch is allowed to land, so a slow
|
|
238
274
|
// response for an old sort/filter can't clobber a newer one.
|
|
239
275
|
let requestSeq = 0
|
|
240
276
|
let disposed = false
|
|
277
|
+
// Once per controller: a misconfigured backend would otherwise log on every
|
|
278
|
+
// page, scroll and filter change.
|
|
279
|
+
let warnedExpressionUnapplied = false
|
|
241
280
|
|
|
242
281
|
const emit = () => {
|
|
243
282
|
state.pageCount = Math.max(1, Math.ceil(state.total / state.pageSize))
|
|
@@ -267,6 +306,23 @@ export function createServerDataSource<TData>(
|
|
|
267
306
|
if (disposed || id !== requestSeq) return // stale
|
|
268
307
|
state.rows = result.rows
|
|
269
308
|
state.total = result.rowCount
|
|
309
|
+
// An expression was sent but the backend did not acknowledge applying it,
|
|
310
|
+
// so these rows are a SUPERSET of what was asked for. Say so instead of
|
|
311
|
+
// filtering the loaded page here: filtering one page would turn
|
|
312
|
+
// "3 of 1,000,000 match" into a confident lie and make paging incoherent,
|
|
313
|
+
// since page 2 would re-filter a different slice.
|
|
314
|
+
state.expressionUnapplied =
|
|
315
|
+
state.filterModel.expression != null && result.appliedExpression !== true
|
|
316
|
+
if (state.expressionUnapplied && !warnedExpressionUnapplied) {
|
|
317
|
+
warnedExpressionUnapplied = true
|
|
318
|
+
console.warn(
|
|
319
|
+
'[svgrid] The data source was sent filterModel.expression but did not ' +
|
|
320
|
+
'return `appliedExpression: true`, so the advanced filter is NOT applied ' +
|
|
321
|
+
'and the rows shown are unfiltered. Apply the whole expression and ' +
|
|
322
|
+
'acknowledge it, or clear the advanced filter. ' +
|
|
323
|
+
'See https://svgrid.com/docs/help/server/server-filtering',
|
|
324
|
+
)
|
|
325
|
+
}
|
|
270
326
|
state.loading = false
|
|
271
327
|
emit()
|
|
272
328
|
} catch (err) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { FilterOperator, Props } from './SvGrid.types'
|
|
5
5
|
import type { GridExportOptions, GridClipboardOptions } from './export-format'
|
|
6
6
|
import type { ChartSpec, ChartType } from './chart'
|
|
7
|
+
import type { GridPredicateExpr } from './filtering/predicate-expr'
|
|
7
8
|
|
|
8
9
|
// Aliased to the core union rather than restated: the API surfaces below hand
|
|
9
10
|
// back whatever the grid actually filtered with, so a hand-maintained subset
|
|
@@ -33,6 +34,13 @@ export type SvGridViewState = {
|
|
|
33
34
|
>
|
|
34
35
|
/** Facet (Excel-style value checklist) selections, keyed by column id. */
|
|
35
36
|
facetFilters: Record<string, string[]>
|
|
37
|
+
/**
|
|
38
|
+
* Advanced-filter expression (Pro). OPTIONAL, and omitted entirely when no
|
|
39
|
+
* advanced filter is set - so views saved before this feature existed, and
|
|
40
|
+
* views from grids that never use it, round-trip byte-identical. An explicit
|
|
41
|
+
* `null` clears the filter on `setState`.
|
|
42
|
+
*/
|
|
43
|
+
advancedFilter?: GridPredicateExpr | null
|
|
36
44
|
/** Built-in chart panel state (present only when `charting` is on): the
|
|
37
45
|
* ACTIVE chart, for back-compat + spread-and-tweak. */
|
|
38
46
|
chart?: {
|
|
@@ -250,10 +258,25 @@ export type SvGridApi<
|
|
|
250
258
|
*/
|
|
251
259
|
refreshEditorOptions(columnId?: string): void
|
|
252
260
|
/**
|
|
253
|
-
* Clear every active column filter (menu, filter-row, set-list, and
|
|
254
|
-
* Resets the grid to "no filtering" in a single call.
|
|
261
|
+
* Clear every active column filter (menu, filter-row, set-list, global, and
|
|
262
|
+
* the advanced filter). Resets the grid to "no filtering" in a single call.
|
|
255
263
|
*/
|
|
256
264
|
clearAllFilters(): void
|
|
265
|
+
/**
|
|
266
|
+
* Set the advanced-filter expression (Pro). `null` clears it. Composed with
|
|
267
|
+
* AND after the global, column and facet filters.
|
|
268
|
+
*
|
|
269
|
+
* Rows are only removed once `@svgrid/enterprise`'s `enableAdvancedFilter()`
|
|
270
|
+
* has registered a compiler. Without it the expression is stored but nothing
|
|
271
|
+
* is filtered - use `isAdvancedFilterActive()` to tell the two apart.
|
|
272
|
+
*/
|
|
273
|
+
setAdvancedFilter(expr: GridPredicateExpr | null): void
|
|
274
|
+
/** The current advanced-filter expression, or `null`. */
|
|
275
|
+
getAdvancedFilter(): GridPredicateExpr | null
|
|
276
|
+
/** Clear the advanced filter, leaving other filter surfaces untouched. */
|
|
277
|
+
clearAdvancedFilter(): void
|
|
278
|
+
/** Whether an expression is set AND an engine is registered to run it. */
|
|
279
|
+
isAdvancedFilterActive(): boolean
|
|
257
280
|
/**
|
|
258
281
|
* Read the active column-menu filters as a snapshot. Keyed by column id.
|
|
259
282
|
* Returns an empty object when nothing is filtered. `valueTo` is only
|
|
@@ -265,9 +288,15 @@ export type SvGridApi<
|
|
|
265
288
|
>
|
|
266
289
|
|
|
267
290
|
/**
|
|
268
|
-
* Snapshot of the rows the grid is
|
|
269
|
-
*
|
|
270
|
-
*
|
|
291
|
+
* Snapshot of the DATA rows the grid is displaying right now - after
|
|
292
|
+
* filtering, sorting, grouping and pagination. Use this when you need the
|
|
293
|
+
* visible result set (e.g. to export it as CSV).
|
|
294
|
+
*
|
|
295
|
+
* Group banner rows are not included: the return type is `TData`, and a
|
|
296
|
+
* banner is not one of your rows. That matters while grouping is on, because
|
|
297
|
+
* with every group collapsed this returns an EMPTY array even though the
|
|
298
|
+
* grid visibly shows a banner per group. Count banners from `getState()`
|
|
299
|
+
* rather than from the length of this.
|
|
271
300
|
*/
|
|
272
301
|
getDisplayedRows(): ReadonlyArray<TData>
|
|
273
302
|
|
|
@@ -290,6 +319,9 @@ export type SvGridApi<
|
|
|
290
319
|
format?: CellFormatConfig
|
|
291
320
|
/** Effective horizontal alignment ('left' | 'center' | 'right'). */
|
|
292
321
|
align?: 'left' | 'center' | 'right'
|
|
322
|
+
/** The column's declared `editorType`, when set. Lets a filter or
|
|
323
|
+
* expression UI offer type-appropriate operators. */
|
|
324
|
+
editorType?: string
|
|
293
325
|
}>
|
|
294
326
|
|
|
295
327
|
// ----- Free data export (CSV / TSV / JSON + clipboard) -----
|