@jielga/tmdatagrid 2.0.0-beta.13 → 2.0.0-beta.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jielga/tmdatagrid",
3
- "version": "2.0.0-beta.13",
3
+ "version": "2.0.0-beta.14",
4
4
  "description": "A React data grid built on TanStack Table v9 and Mantine - always virtualized, with resizable, reorderable, sortable, filterable, hideable and pinnable columns.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,7 @@ description: >
17
17
  metadata:
18
18
  type: core
19
19
  library: '@jielga/tmdatagrid'
20
- library_version: '2.0.0-beta.13'
20
+ library_version: '2.0.0-beta.14'
21
21
  sources:
22
22
  - 'Jielga/TMDataGrid:src/docs/styling.md'
23
23
  - 'Jielga/TMDataGrid:src/docs/toolbar.md'
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '2.0.0-beta.13'
17
+ library_version: '2.0.0-beta.14'
18
18
  sources:
19
19
  - 'Jielga/TMDataGrid:src/docs/cell-selection.md'
20
20
  - 'Jielga/TMDataGrid:src/tmdatagrid/core/cellNavigation.ts'
@@ -4,19 +4,19 @@ description: >
4
4
  Define and arrange TMDataGrid columns. Covers createTMDataGridColumnHelper,
5
5
  every column meta field (label, type, options, flex, align, autoSize,
6
6
  enableOrdering, and the meta.filter and meta.edit namespaces holding
7
- defaultOperator, control, enabled, field, editor, validate and mapValue), the
8
- six column types, fluid minmax sizing versus fixed width with minSize /
9
- maxSize / size, autosizing and autosizeColumn, hiding through enableHiding and
10
- the columns panel, pinning and why a pinned column becomes fixed-width,
11
- ordering with enableColumnOrdering, meta.enableOrdering, moveColumn,
12
- moveColumnByStep, getStepTargetColumn and the pinned regions, resetSettings,
13
- sorting with multi-sort through isMultiSortEvent and a custom sortFn, and the
14
- generated lanes. Load when adding or changing columns, controlling widths,
15
- hiding, pinning, reordering or sorting them.
7
+ operators, defaultOperator, control, enabled, field, editor, validate and
8
+ mapValue), the six column types, fluid minmax sizing versus fixed width with
9
+ minSize / maxSize / size, autosizing and autosizeColumn, hiding through
10
+ enableHiding and the columns panel, pinning and why a pinned column becomes
11
+ fixed-width, ordering with enableColumnOrdering, meta.enableOrdering,
12
+ moveColumn, moveColumnByStep, getStepTargetColumn and the pinned regions,
13
+ resetSettings, sorting with multi-sort through isMultiSortEvent and a custom
14
+ sortFn, and the generated lanes. Load when adding or changing columns,
15
+ controlling widths, hiding, pinning, reordering or sorting them.
16
16
  metadata:
17
17
  type: core
18
18
  library: '@jielga/tmdatagrid'
19
- library_version: '2.0.0-beta.13'
19
+ library_version: '2.0.0-beta.14'
20
20
  sources:
21
21
  - 'Jielga/TMDataGrid:src/docs/columns.md'
22
22
  - 'Jielga/TMDataGrid:src/docs/column-layout.md'
@@ -97,7 +97,8 @@ which is why they are in neither.
97
97
 
98
98
  | Field | Type | Default | What it does |
99
99
  | --- | --- | --- | --- |
100
- | `defaultOperator` | `TMDataGridFilterOperator` | The type's default | The operator a fresh filter on this column starts with. |
100
+ | `operators` | `readonly TMDataGridFilterOperator[]` | The type's list | The operators this column offers, a subset of its type's. For a backend that answers only some. |
101
+ | `defaultOperator` | `TMDataGridFilterOperator` | The type's default, else the first offered | The operator a fresh filter on this column starts with. |
101
102
  | `control` | `TMDataGridFilterControlComponent` | By `meta.type` | Replaces the value control in this column's filter row. Module scope. |
102
103
 
103
104
  `meta.edit`:
@@ -15,7 +15,7 @@ description: >
15
15
  metadata:
16
16
  type: core
17
17
  library: '@jielga/tmdatagrid'
18
- library_version: '2.0.0-beta.13'
18
+ library_version: '2.0.0-beta.14'
19
19
  sources:
20
20
  - 'Jielga/TMDataGrid:src/docs/pagination.md'
21
21
  - 'Jielga/TMDataGrid:src/docs/scrolling.md'
@@ -18,7 +18,7 @@ description: >
18
18
  metadata:
19
19
  type: core
20
20
  library: '@jielga/tmdatagrid'
21
- library_version: '2.0.0-beta.13'
21
+ library_version: '2.0.0-beta.14'
22
22
  sources:
23
23
  - 'Jielga/TMDataGrid:src/docs/editing.md'
24
24
  - 'Jielga/TMDataGrid:src/docs/query-builder.md'
@@ -3,9 +3,10 @@ name: filtering
3
3
  description: >
4
4
  Narrow the rows a TMDataGrid shows. Covers the shared tmDataGrid filter
5
5
  function and its {operator, value} model, the eighteen operators and which
6
- meta.type offers each, meta.filter.defaultOperator, isFilterActive and the
7
- half-typed filter, the filters option and its surfaces (popup, sidebar, none,
8
- plus inHeader for header filters), TMDataGrid.FilterPanel and its layout prop,
6
+ meta.type offers each, meta.filter.operators to offer a column only a subset
7
+ of them, meta.filter.defaultOperator, isFilterActive and the half-typed
8
+ filter, the filters option and its surfaces (popup, sidebar, none, plus
9
+ inHeader for header filters), TMDataGrid.FilterPanel and its layout prop,
9
10
  TMDataGrid.FilterButton, TMDataGrid.FilterPills with its api prop,
10
11
  openColumnFilter, replacing a value control with DgRangeSliderFilter /
11
12
  DgDateRangeFilter / DgAutocompleteFilter / DgTriStateFilter or a
@@ -17,7 +18,7 @@ description: >
17
18
  metadata:
18
19
  type: core
19
20
  library: '@jielga/tmdatagrid'
20
- library_version: '2.0.0-beta.13'
21
+ library_version: '2.0.0-beta.14'
21
22
  sources:
22
23
  - 'Jielga/TMDataGrid:src/docs/filtering.md'
23
24
  - 'Jielga/TMDataGrid:src/docs/quick-search.md'
@@ -84,6 +85,25 @@ columnHelper.accessor("salary", {
84
85
  });
85
86
  ```
86
87
 
88
+ `meta.filter.operators` narrows the list a column offers to a subset of its
89
+ type's - for a column whose backend answers only some operators, so the user is
90
+ never offered one the query cannot express. The panel dropdown and the header
91
+ funnel show only those, in the type's order. An operator the type does not
92
+ offer is ignored; a list that leaves nothing falls back to the type's full set.
93
+ Without `defaultOperator`, a fresh filter opens on the type's default when it is
94
+ offered, else on the first offered operator.
95
+
96
+ ```tsx
97
+ columnHelper.accessor("customer", {
98
+ header: "Customer",
99
+ meta: { filter: { operators: ["contains", "equals", "isEmpty", "isNotEmpty"] } },
100
+ });
101
+ ```
102
+
103
+ `getColumnOperators(column)` returns the resolved list and
104
+ `getColumnDefaultOperator(column)` the operator a fresh filter opens on. The
105
+ `server-side` skill shows the list typed together with the API mapping table.
106
+
87
107
  ## The filters option
88
108
 
89
109
  `filters` on `useTMDataGrid` picks the surface. It is read field by field, so a
@@ -400,7 +420,8 @@ Source: `src/docs/quick-search.md` (Fuzzy by default).
400
420
  | `enableColumnFilters` | Table option | `boolean` | `true` | `false` removes the panel, the button and the menu item. |
401
421
  | `enableColumnFilter` | Column option | `boolean` | `true` | `false` takes one column out of filtering. |
402
422
  | `meta.type` | Column meta | `TMDataGridColumnType` | `"string"` | Selects the operators and the value control. |
403
- | `meta.filter.defaultOperator` | Column meta | `TMDataGridFilterOperator` | The type's default | The operator a fresh filter opens on. |
423
+ | `meta.filter.operators` | Column meta | `readonly TMDataGridFilterOperator[]` | The type's list | The operators this column offers, a subset of its type's. |
424
+ | `meta.filter.defaultOperator` | Column meta | `TMDataGridFilterOperator` | The type's default, else the first offered | The operator a fresh filter opens on. |
404
425
  | `meta.filter.control` | Column meta | `TMDataGridFilterControlComponent` | By type and operator | Replaces the value control. Module scope. |
405
426
  | `filterFn` | Column option | name or fn | `"tmDataGrid"` | Custom matching for one column. |
406
427
  | `quickSearchMode` | Option | `"fuzzy" \| "contains"` | `"fuzzy"` | How the quick search matches. |
@@ -418,6 +439,7 @@ Source: `src/docs/quick-search.md` (Fuzzy by default).
418
439
  | `isFilterActive` | Export | `(value) => boolean` | – | Whether a filter value narrows anything. |
419
440
  | `activeColumnFilters` | Export | `(columnFilters \| table) => Array<{ id, value }>` | – | The filters in the grid's own value shape that narrow anything, typed. |
420
441
  | `getOperatorsForType` | Export | `(type) => operators` | – | The operator list a type offers. |
442
+ | `getColumnOperators` · `getColumnDefaultOperator` | Exports | `(column) => operators` · `(column) => operator` | – | One column's list after `meta.filter.operators`, and the operator a fresh filter on it opens on. |
421
443
  | `FILTER_OPERATOR_LABELS` | Export | record | – | The label shown for each operator. |
422
444
  | `formatFilterLabel` | Export | `({ label, type, filter }) => string` | – | The one-line description used on the pills. |
423
445
  | `emptyValueForOperator` · `operatorNeedsValue` · `operatorTakesArrayValue` · `operatorTakesRangeValue` | Exports | – | – | What shape of value an operator expects. |
@@ -11,7 +11,7 @@ description: >
11
11
  metadata:
12
12
  type: core
13
13
  library: '@jielga/tmdatagrid'
14
- library_version: '2.0.0-beta.13'
14
+ library_version: '2.0.0-beta.14'
15
15
  sources:
16
16
  - 'Jielga/TMDataGrid:src/docs/getting-started.md'
17
17
  - 'Jielga/TMDataGrid:src/docs/anatomy.md'
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '2.0.0-beta.13'
17
+ library_version: '2.0.0-beta.14'
18
18
  sources:
19
19
  - 'Jielga/TMDataGrid:src/docs/grouping.md'
20
20
  - 'Jielga/TMDataGrid:src/docs/summary-row.md'
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '2.0.0-beta.13'
17
+ library_version: '2.0.0-beta.14'
18
18
  sources:
19
19
  - 'Jielga/TMDataGrid:src/docs/use-tm-data-grid.md'
20
20
  - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
@@ -17,7 +17,7 @@ description: >
17
17
  metadata:
18
18
  type: core
19
19
  library: '@jielga/tmdatagrid'
20
- library_version: '2.0.0-beta.13'
20
+ library_version: '2.0.0-beta.14'
21
21
  sources:
22
22
  - 'Jielga/TMDataGrid:src/docs/row-selection.md'
23
23
  - 'Jielga/TMDataGrid:src/docs/row-interaction.md'
@@ -4,16 +4,21 @@ description: >
4
4
  Drive TMDataGrid from a server with TanStack manual modes - manualPagination,
5
5
  manualSorting, manualFiltering, rowCount, controlled state and onXChange
6
6
  callbacks. Covers the loading and totalRowCount meta fields, forwarding the
7
- plain-JSON columnFilters model to an API with activeColumnFilters, the
8
- first-page reset on a query change, persistence interaction, and row selection
9
- across pages. Load when the grid is backed by a paginated API rather than a
10
- local array.
7
+ plain-JSON columnFilters model to an API with activeColumnFilters, mapping
8
+ filters, sorting and the page index onto an endpoint's own query language
9
+ (field table, operator table, the three value shapes, meta.filter.operators
10
+ for an endpoint that answers only some operators, keying the fetch on the
11
+ request, paging against a page envelope), the first-page reset on a query
12
+ change, persistence interaction, and row selection across pages. Load when the
13
+ grid is backed by a paginated API rather than a local array, or when
14
+ translating grid filters into server-side queries.
11
15
  metadata:
12
16
  type: core
13
17
  library: '@jielga/tmdatagrid'
14
- library_version: '2.0.0-beta.13'
18
+ library_version: '2.0.0-beta.14'
15
19
  sources:
16
20
  - 'Jielga/TMDataGrid:src/docs/server-side.md'
21
+ - 'Jielga/TMDataGrid:src/docs/server-query.md'
17
22
  - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
18
23
  ---
19
24
 
@@ -124,6 +129,118 @@ value - a custom filter control writing raw values - is dropped.
124
129
 
125
130
  Debounce requests. The filter value input updates on every keystroke.
126
131
 
132
+ ## Mapping onto the endpoint's query language
133
+
134
+ An API takes a request body of its own: its own field names, its own operator
135
+ set, its own status codes, and pages counted from 1. The layer between the
136
+ grid's state and that body is one function over two lookup tables, plus one
137
+ function on the way back:
138
+
139
+ - **A field table** keyed by column id, giving the API field and the cast from
140
+ the string every filter control writes to the type the field holds
141
+ (`Number`, an enum code). A column missing from the table is one the API
142
+ cannot query: the mapping drops the filter rather than sending a field the
143
+ endpoint would reject.
144
+ - **An operator table** from `TMDataGridFilterOperator` to the API's operators.
145
+ Several grid operators collapse onto one - a date `before` and a number
146
+ `lessThan` are both `lt` once the value is cast. Declare it as a `Record`, not
147
+ a `Partial`, so an operator added by a later grid version fails the build
148
+ here rather than reaching the server unmapped.
149
+ - **`toRow`** from the API's record to the grid's row type.
150
+
151
+ ```ts
152
+ const QUERY_FIELDS: Record<string, { field: string; cast: (raw: string) => string | number }> = {
153
+ id: { field: "orderRef", cast: Number },
154
+ amount: { field: "totalAmount", cast: Number },
155
+ status: { field: "status", cast: (raw) => STATUS_CODES[raw] ?? raw },
156
+ };
157
+
158
+ const PREDICATE_OPS: Record<TMDataGridFilterOperator, PredicateOp> = {
159
+ contains: "like",
160
+ between: "range",
161
+ before: "lt",
162
+ lessThan: "lt",
163
+ isAnyOf: "in",
164
+ isEmpty: "isNull",
165
+ // ...one line for every remaining operator.
166
+ };
167
+ ```
168
+
169
+ ### The three value shapes
170
+
171
+ `TMDataGridFilterValue` is `{ operator, value }`, and the operator decides what
172
+ `value` holds. Branch on all four cases, in this order:
173
+
174
+ | Operator | `value` | Sent as |
175
+ | --- | --- | --- |
176
+ | `isEmpty`, `isNotEmpty` | Not used | `{ field, op }` |
177
+ | `isAnyOf`, `isNoneOf` | `ReadonlyArray<string>` | `{ field, op, values }` |
178
+ | `between` | `[min, max]`, either end possibly `""` | `{ field, op, from?, to? }` |
179
+ | Everything else | `string` | `{ field, op, value }` |
180
+
181
+ An empty end of a `between` pair leaves that side open: an absent bound, not an
182
+ empty string. Run `activeColumnFilters` over the slice first so a half-typed
183
+ filter is not sent as a predicate that narrows the result to nothing.
184
+
185
+ ### An endpoint that answers only some operators
186
+
187
+ Most endpoints do not have every operator the grid has - `like` and `eq` but no
188
+ prefix match is common. Do not offer what you would have to drop.
189
+ `meta.filter.operators` narrows the column to the operators the query can
190
+ express, and the mapping table is declared over exactly that list, so one
191
+ cannot be offered without a mapping or mapped without being offered:
192
+
193
+ ```ts
194
+ const TEXT_OPERATORS = [
195
+ "contains",
196
+ "equals",
197
+ "isEmpty",
198
+ "isNotEmpty",
199
+ ] as const satisfies readonly TMDataGridFilterOperator[];
200
+
201
+ const TEXT_OPS: Record<(typeof TEXT_OPERATORS)[number], PredicateOp> = {
202
+ contains: "like",
203
+ equals: "eq",
204
+ isEmpty: "isNull",
205
+ isNotEmpty: "isNotNull",
206
+ };
207
+
208
+ columnHelper.accessor("customer", {
209
+ header: "Customer",
210
+ meta: { filter: { operators: TEXT_OPERATORS } },
211
+ });
212
+ ```
213
+
214
+ A fresh filter opens on `meta.filter.defaultOperator` when set, else on the
215
+ type's default when the list holds it, else on the first entry. The lookup at
216
+ the boundary still returns `undefined` for an unmapped operator: a filter
217
+ restored by `persist` from before the list was narrowed can carry one.
218
+
219
+ ### Keying the fetch on the request
220
+
221
+ Serialize the request with `JSON.stringify` inside `useMemo` over
222
+ `columnFilters`, `sorting` and `pagination`, and key the fetch effect (or the
223
+ TanStack Query `queryKey`) on that string. Opening the panel and adding an
224
+ empty row moves `columnFilters` but leaves the request unchanged, so nothing is
225
+ sent. The effect owes the server a debounce (the value input updates on every
226
+ keystroke) and a cancel (a `cancelled` flag in the cleanup, or an
227
+ `AbortController` on a real `fetch`).
228
+
229
+ ### Paging against a page envelope
230
+
231
+ | The API's | The grid's | Written as |
232
+ | --- | --- | --- |
233
+ | `page.number`, counted from 1 | `pagination.pageIndex`, counted from 0 | `number: pageIndex + 1` |
234
+ | `page.totalItems`, the matched count | `rowCount` | `rowCount: page?.totalItems ?? 0` |
235
+ | `page.totalPages` | `state.pageCount`, derived from `rowCount / pageSize` | Nothing; the grid computes it |
236
+
237
+ Forward `totalPages` only when the server pages by something other than the
238
+ size the grid asked for. `pageCount: -1` when the total is unknown. Show the
239
+ page number through the Footer's `renderPagination` slot with
240
+ `<Controls.PageSize /><Controls.PageNumber /><Controls.Pager />`.
241
+ `meta.totalRowCount` is the unfiltered total, which no filtered response
242
+ carries: take it from a separate count call.
243
+
127
244
  ## Persistence
128
245
 
129
246
  `persist` works unchanged. `dataKey` restores filters, sorting and pagination
@@ -168,6 +285,13 @@ current page, so the grid renders the matched count alone rather than a
168
285
  plausible-looking wrong total. Pass the unfiltered total as
169
286
  `meta.totalRowCount` to get the "42 / 5000" form back.
170
287
 
288
+ ### Offering operators the endpoint cannot answer
289
+
290
+ The panel offers every operator of the column's type, and a mapping that drops
291
+ `startsWith` leaves the user with a filter that silently does nothing. Declare
292
+ `meta.filter.operators` on the column with the operators the endpoint answers,
293
+ and type the operator table over that same list.
294
+
171
295
  ### Unstable getRowId across pages
172
296
 
173
297
  Defaulting to the row index means row 0 of page 2 shares an id with row 0 of
@@ -11,7 +11,7 @@ description: >
11
11
  metadata:
12
12
  type: core
13
13
  library: '@jielga/tmdatagrid'
14
- library_version: '2.0.0-beta.13'
14
+ library_version: '2.0.0-beta.14'
15
15
  sources:
16
16
  - 'Jielga/TMDataGrid:src/docs/testing.md'
17
17
  - 'Jielga/TMDataGrid:src/tmdatagrid/components/TMDataGrid.tsx'
@@ -55,26 +55,7 @@ export type TMDataGridProps<TData extends RowData> = TMDataGridApi<TData> & {
55
55
  "data-testid"?: string;
56
56
  };
57
57
 
58
- /**
59
- * Root of the grid. Takes the object returned by `useTMDataGrid` - spread it -
60
- * and publishes it to the compound components below it:
61
- *
62
- * ```tsx
63
- * const grid = useTMDataGrid({ data, columns });
64
- *
65
- * <TMDataGrid {...grid}>
66
- * <TMDataGrid.Toolbar>
67
- * <TMDataGrid.SummaryCount />
68
- * <TMDataGrid.Spacer />
69
- * <TMDataGrid.Menu>
70
- * <TMDataGrid.Menu.Columns />
71
- * </TMDataGrid.Menu>
72
- * </TMDataGrid.Toolbar>
73
- * <TMDataGrid.Table />
74
- * <TMDataGrid.Footer />
75
- * </TMDataGrid>
76
- * ```
77
- */
58
+ // Documented on the `TMDataGrid` export below.
78
59
  function TMDataGridRoot<TData extends RowData>({
79
60
  table,
80
61
  ui,
@@ -150,6 +131,26 @@ function TMDataGridRoot<TData extends RowData>({
150
131
  );
151
132
  }
152
133
 
134
+ /**
135
+ * Root of the grid. Takes the object returned by `useTMDataGrid` - spread it -
136
+ * and publishes it to the compound components below it:
137
+ *
138
+ * ```tsx
139
+ * const grid = useTMDataGrid({ data, columns });
140
+ *
141
+ * <TMDataGrid {...grid}>
142
+ * <TMDataGrid.Toolbar>
143
+ * <TMDataGrid.SummaryCount />
144
+ * <TMDataGrid.Spacer />
145
+ * <TMDataGrid.Menu>
146
+ * <TMDataGrid.Menu.Columns />
147
+ * </TMDataGrid.Menu>
148
+ * </TMDataGrid.Toolbar>
149
+ * <TMDataGrid.Table />
150
+ * <TMDataGrid.Footer />
151
+ * </TMDataGrid>
152
+ * ```
153
+ */
153
154
  export const TMDataGrid = Object.assign(TMDataGridRoot, {
154
155
  Toolbar: TMDataGridToolbar,
155
156
  Spacer: TMDataGridToolbarSpacer,
@@ -5,6 +5,7 @@ import classes from "./TMDataGridFilterPanel.module.css";
5
5
  import { useTMDataGridContext } from "../TMDataGridContext";
6
6
  import {
7
7
  getColumnDefaultOperator,
8
+ getColumnOperators,
8
9
  getColumnLabel,
9
10
  getColumnType,
10
11
  } from "../core/columnUtils";
@@ -251,7 +252,10 @@ export function TMDataGridFilterPanel({
251
252
  w={stacked ? "100%" : 170}
252
253
  allowDeselect={false}
253
254
  data-dg-part="filter-operator"
254
- data={getOperatorsForType(type).map((operator) => ({
255
+ data={(column
256
+ ? getColumnOperators(column)
257
+ : getOperatorsForType(type)
258
+ ).map((operator) => ({
255
259
  value: operator,
256
260
  label: labels.operators[operator],
257
261
  }))}
@@ -7,7 +7,7 @@ import { useTMDataGridContext } from "../TMDataGridContext";
7
7
  import {
8
8
  getColumnDefaultOperator,
9
9
  getColumnLabel,
10
- getColumnType,
10
+ getColumnOperators,
11
11
  isControlColumn,
12
12
  } from "../core/columnUtils";
13
13
  import {
@@ -15,7 +15,6 @@ import {
15
15
  type TMDataGridFilterValue,
16
16
  emptyValueForOperator,
17
17
  filterValueShape,
18
- getOperatorsForType,
19
18
  isTMDataGridFilterValue,
20
19
  } from "../core/filterOperators";
21
20
  import { FilterIcon } from "./icons";
@@ -54,7 +53,6 @@ function HeaderFilterControl({ column }: { column: TMDataGridHeader["column"] })
54
53
  (state) => state.columnFilters.find((filter) => filter.id === column.id)?.value,
55
54
  );
56
55
 
57
- const type = getColumnType(column);
58
56
  const defaultOperator = getColumnDefaultOperator(column);
59
57
  const current = isTMDataGridFilterValue(filterValue) ? filterValue : undefined;
60
58
  const operator = current?.operator ?? defaultOperator;
@@ -104,7 +102,7 @@ function HeaderFilterControl({ column }: { column: TMDataGridHeader["column"] })
104
102
  // eslint-disable-next-line react-hooks/exhaustive-deps
105
103
  [table, column, facetKey],
106
104
  );
107
- const operators = getOperatorsForType(type);
105
+ const operators = getColumnOperators(column);
108
106
 
109
107
  return (
110
108
  <>
@@ -44,16 +44,7 @@ export type TMDataGridMenuColumnsProps = {
44
44
  searchable?: boolean;
45
45
  };
46
46
 
47
- /**
48
- * Burger menu in the grid's top-right corner, holding whatever the consumer
49
- * puts in it. Every Mantine `Menu` prop is accepted and wins over the defaults
50
- * below.
51
- *
52
- * It always renders. It cannot know whether its children render anything, so
53
- * a menu holding only `TMDataGrid.Menu.Columns` on a grid where nothing can be
54
- * hidden still shows its button over an empty dropdown - leave the menu out of
55
- * the toolbar in that case.
56
- */
47
+ // Documented on the `TMDataGridMenu` export below.
57
48
  function TMDataGridMenuRoot({
58
49
  children,
59
50
  icon,
@@ -247,6 +238,16 @@ export function TMDataGridMenuResetLayout() {
247
238
  );
248
239
  }
249
240
 
241
+ /**
242
+ * Burger menu in the grid's top-right corner, holding whatever the consumer
243
+ * puts in it. Every Mantine `Menu` prop is accepted and wins over the defaults
244
+ * below.
245
+ *
246
+ * It always renders. It cannot know whether its children render anything, so
247
+ * a menu holding only `TMDataGrid.Menu.Columns` on a grid where nothing can be
248
+ * hidden still shows its button over an empty dropdown - leave the menu out of
249
+ * the toolbar in that case.
250
+ */
250
251
  export const TMDataGridMenu = Object.assign(TMDataGridMenuRoot, {
251
252
  Columns: TMDataGridMenuColumns,
252
253
  ColumnToggles: TMDataGridMenuColumnToggles,
@@ -1,6 +1,7 @@
1
1
  import type { Row } from "@tanstack/react-table";
2
2
  import {
3
3
  getDefaultOperator,
4
+ getOperatorsForType,
4
5
  type TMDataGridColumnType,
5
6
  type TMDataGridFilterOperator,
6
7
  } from "./filterOperators";
@@ -37,17 +38,36 @@ export function getColumnType(column: ColumnLike): TMDataGridColumnType {
37
38
  return column.columnDef.meta?.type ?? "string";
38
39
  }
39
40
 
41
+ /**
42
+ * The operators this column offers: the type's list, narrowed to
43
+ * `meta.filter.operators` when the column declares one. The type's order is
44
+ * kept so the menu reads the same on every column; an operator the type does
45
+ * not offer is dropped, and an allowlist that leaves nothing falls back to the
46
+ * type's full list rather than an empty menu.
47
+ */
48
+ export function getColumnOperators(
49
+ column: ColumnLike,
50
+ ): readonly TMDataGridFilterOperator[] {
51
+ const offered = getOperatorsForType(getColumnType(column));
52
+ const allowed = column.columnDef.meta?.filter?.operators;
53
+ if (!allowed) return offered;
54
+ const narrowed = offered.filter((operator) => allowed.includes(operator));
55
+ return narrowed.length > 0 ? narrowed : offered;
56
+ }
57
+
40
58
  /**
41
59
  * The operator a fresh filter on this column starts with -
42
- * `meta.filter.defaultOperator`, else the type's default.
60
+ * `meta.filter.defaultOperator`, else the type's default where the column
61
+ * offers it, else the first operator it does offer.
43
62
  */
44
63
  export function getColumnDefaultOperator(
45
64
  column: ColumnLike,
46
65
  ): TMDataGridFilterOperator {
47
- return (
48
- column.columnDef.meta?.filter?.defaultOperator ??
49
- getDefaultOperator(getColumnType(column))
50
- );
66
+ const declared = column.columnDef.meta?.filter?.defaultOperator;
67
+ if (declared) return declared;
68
+ const offered = getColumnOperators(column);
69
+ const typeDefault = getDefaultOperator(getColumnType(column));
70
+ return offered.includes(typeDefault) ? typeDefault : offered[0];
51
71
  }
52
72
 
53
73
  /**
@@ -86,10 +86,19 @@ export type TMDataGridFilterControlComponent =
86
86
  * declaration of each feeds the filter panel and the cell editor alike.
87
87
  */
88
88
  export type TMDataGridColumnFilterOptions = {
89
+ /**
90
+ * The operators this column offers, a subset of the type's own. For a
91
+ * column backed by an endpoint that answers only some of them - `contains`
92
+ * and `equals`, say - so the panel and the header funnel never offer an
93
+ * operator the query cannot express. Kept in the type's order; one the type
94
+ * does not offer is ignored, and a list that leaves nothing falls back to
95
+ * the type's full set.
96
+ */
97
+ operators?: readonly TMDataGridFilterOperator[];
89
98
  /**
90
99
  * The operator a fresh filter on this column starts with, instead of the
91
100
  * type's default - a salary column can open on `"between"`. Must be one of
92
- * the type's own operators.
101
+ * the operators the column offers.
93
102
  */
94
103
  defaultOperator?: TMDataGridFilterOperator;
95
104
  /**
@@ -147,6 +147,7 @@ export { TMDATAGRID_LABELS_SV } from "./core/labelsSv";
147
147
  export {
148
148
  getColumnDefaultOperator,
149
149
  getColumnFilterControl,
150
+ getColumnOperators,
150
151
  getColumnLabel,
151
152
  getColumnType,
152
153
  isColumnEditableForRow,
@@ -186,8 +186,8 @@ export type TMDataGridColumnMeta = {
186
186
  */
187
187
  enableOrdering?: boolean;
188
188
  /**
189
- * How this column filters: the operator a fresh filter starts with, and the
190
- * value control the filter panel renders for it.
189
+ * How this column filters: which operators it offers, the operator a fresh
190
+ * filter starts with, and the value control the filter panel renders for it.
191
191
  *
192
192
  * ```tsx
193
193
  * meta: {