@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.
Files changed (66) hide show
  1. package/dist/SvGrid.controller.svelte.d.ts +2 -1
  2. package/dist/SvGrid.controller.svelte.js +145 -3
  3. package/dist/SvGrid.css +32 -0
  4. package/dist/SvGrid.svelte +64 -6
  5. package/dist/SvGrid.types.d.ts +61 -0
  6. package/dist/a11y/grid-announcements.d.ts +58 -0
  7. package/dist/a11y/grid-announcements.js +70 -0
  8. package/dist/advanced-filter.svelte.d.ts +42 -0
  9. package/dist/advanced-filter.svelte.js +13 -0
  10. package/dist/build-api.js +44 -2
  11. package/dist/cdn/{GridMenus-BoVKgAv8.js → GridMenus-CBvjL5u4.js} +14 -14
  12. package/dist/cdn/{GridMenus-DL1zrDwf.js → GridMenus-DAzp6YXc.js} +14 -14
  13. package/dist/cdn/{src-M2NfKnjX.js → src-3WKN7L4G.js} +5932 -5743
  14. package/dist/cdn/{src-CEpw6Ato.js → src-Cl7h5-Ux.js} +3371 -3182
  15. package/dist/cdn/svgrid.js +8 -8
  16. package/dist/cdn/svgrid.svelte-external.js +8 -8
  17. package/dist/filter-operators.d.ts +2 -1
  18. package/dist/filter-operators.js +5 -6
  19. package/dist/filtering/excel-filters.d.ts +1 -0
  20. package/dist/filtering/excel-filters.js +5 -0
  21. package/dist/filtering/filter-operator-catalogue.d.ts +42 -0
  22. package/dist/filtering/filter-operator-catalogue.js +42 -0
  23. package/dist/filtering/predicate-expr.d.ts +78 -0
  24. package/dist/filtering/predicate-expr.js +1 -0
  25. package/dist/filtering/row-predicate.d.ts +17 -0
  26. package/dist/filtering/row-predicate.js +55 -0
  27. package/dist/grid-messages.d.ts +7 -0
  28. package/dist/grid-messages.js +7 -0
  29. package/dist/index.d.ts +4 -0
  30. package/dist/index.js +5 -0
  31. package/dist/server-data-source.d.ts +34 -0
  32. package/dist/server-data-source.js +19 -0
  33. package/dist/svgrid-wrapper.types.d.ts +37 -5
  34. package/dist/themes/index.js +12 -4
  35. package/package.json +2 -1
  36. package/src/SvGrid.controller.svelte.ts +169 -2
  37. package/src/SvGrid.css +32 -0
  38. package/src/SvGrid.svelte +64 -6
  39. package/src/SvGrid.types.ts +61 -0
  40. package/src/a11y/grid-announcements.test.ts +78 -0
  41. package/src/a11y/grid-announcements.ts +94 -0
  42. package/src/a11y.announce.test.ts +250 -0
  43. package/src/a11y.axe.test.ts +194 -0
  44. package/src/advanced-filter.svelte.ts +59 -0
  45. package/src/build-api.ts +43 -2
  46. package/src/filter-operators.ts +9 -6
  47. package/src/filtering/excel-filters.ts +15 -0
  48. package/src/filtering/filter-operator-catalogue.ts +74 -0
  49. package/src/filtering/node-entry.test.ts +75 -0
  50. package/src/filtering/predicate-expr.ts +50 -0
  51. package/src/filtering/row-predicate.ts +57 -0
  52. package/src/grid-messages.ts +18 -0
  53. package/src/index.ts +28 -0
  54. package/src/server-data-source.test.ts +94 -0
  55. package/src/server-data-source.ts +56 -0
  56. package/src/svgrid-wrapper.types.ts +37 -5
  57. package/src/svgrid.advanced-filter.test.ts +481 -0
  58. package/src/svgrid.displayed-rows.test.ts +144 -0
  59. package/src/svgrid.group-props-reactive.svelte.test.ts +151 -0
  60. package/src/svgrid.group-props.test.ts +280 -0
  61. package/src/themes/contrast.test.ts +112 -0
  62. package/src/themes/index.ts +12 -4
  63. package/themes/catppuccin.css +3 -3
  64. package/themes/dracula.css +3 -3
  65. package/themes/nord.css +3 -3
  66. package/themes/notion.css +4 -4
@@ -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 global).
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 actually displaying right now -
269
- * after filtering, sorting, grouping, and pagination have been applied.
270
- * Use this when you need to export the visible result set (e.g. CSV).
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) -----