@object-ui/components 17.5.0 → 17.7.0

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 (73) hide show
  1. package/CHANGELOG.md +10155 -0
  2. package/README.md +20 -10
  3. package/dist/custom/button-group.d.ts +2 -2
  4. package/dist/custom/character-count.d.ts +56 -0
  5. package/dist/custom/combobox.d.ts +2 -4
  6. package/dist/custom/config-field-renderer.d.ts +1 -1
  7. package/dist/custom/config-panel-renderer.d.ts +3 -3
  8. package/dist/custom/empty.d.ts +16 -4
  9. package/dist/custom/field.d.ts +1 -1
  10. package/dist/custom/filter-builder.d.ts +448 -36
  11. package/dist/custom/fullscreen-editor.d.ts +48 -2
  12. package/dist/custom/index.d.ts +27 -28
  13. package/dist/custom/item.d.ts +2 -2
  14. package/dist/custom/kbd.d.ts +1 -1
  15. package/dist/custom/mobile-dialog-content.d.ts +22 -1
  16. package/dist/custom/navigation-overlay.d.ts +105 -0
  17. package/dist/custom/refresh-indicator.d.ts +8 -2
  18. package/dist/custom/resizable.d.ts +1 -1
  19. package/dist/custom/view-states.d.ts +74 -1
  20. package/dist/debug/index.d.ts +2 -2
  21. package/dist/hooks/related-count-store.d.ts +26 -28
  22. package/dist/index.css +174 -364
  23. package/dist/index.d.ts +21 -21
  24. package/dist/index.js +7274 -6350
  25. package/dist/index.umd.cjs +12 -13
  26. package/dist/lib/close-label.d.ts +1 -2
  27. package/dist/lib/date-fns-locale.d.ts +41 -0
  28. package/dist/lib/form-control-dom-props.d.ts +77 -0
  29. package/dist/lib/lazy-icon.d.ts +32 -1
  30. package/dist/lib/lucide-record-icon-names.d.ts +30 -0
  31. package/dist/lib/slider-thumb.d.ts +86 -0
  32. package/dist/lib/utils.d.ts +113 -0
  33. package/dist/notifications/index.d.ts +5 -5
  34. package/dist/renderers/action/action-bar.d.ts +8 -36
  35. package/dist/renderers/action/action-button.d.ts +15 -4
  36. package/dist/renderers/action/action-group.d.ts +5 -4
  37. package/dist/renderers/action/action-icon.d.ts +12 -2
  38. package/dist/renderers/action/action-menu.d.ts +5 -2
  39. package/dist/renderers/action/auto-trigger.d.ts +19 -4
  40. package/dist/renderers/action/resolve-icon.d.ts +27 -2
  41. package/dist/renderers/action/static-params.d.ts +79 -0
  42. package/dist/renderers/action/visibility-gate.d.ts +11 -3
  43. package/dist/renderers/basic/elements.d.ts +6 -3
  44. package/dist/renderers/basic/html-elements.d.ts +8 -4
  45. package/dist/renderers/basic/readProps.d.ts +60 -0
  46. package/dist/renderers/complex/data-table.d.ts +31 -14
  47. package/dist/renderers/complex/dataTableBindDiagnostic.d.ts +187 -0
  48. package/dist/renderers/form/validationRuleKeys.d.ts +45 -0
  49. package/dist/renderers/layout/box.d.ts +8 -0
  50. package/dist/renderers/layout/containers.d.ts +1 -10
  51. package/dist/renderers/layout/react-page.d.ts +16 -2
  52. package/dist/renderers/navigation/sidebar.d.ts +31 -0
  53. package/dist/share/index.d.ts +2 -2
  54. package/dist/ui/badge.d.ts +1 -1
  55. package/dist/ui/button.d.ts +1 -1
  56. package/dist/ui/calendar.d.ts +3 -2
  57. package/dist/ui/carousel.d.ts +2 -2
  58. package/dist/ui/command.d.ts +8 -8
  59. package/dist/ui/form.d.ts +1 -1
  60. package/dist/ui/index.d.ts +46 -47
  61. package/dist/ui/input-otp.d.ts +2 -2
  62. package/dist/ui/pagination.d.ts +1 -1
  63. package/dist/ui/sheet.d.ts +1 -1
  64. package/dist/ui/sidebar.d.ts +4 -3
  65. package/dist/ui/slider.d.ts +2 -1
  66. package/package.json +17 -18
  67. package/dist/SchemaRenderer.d.ts +0 -4
  68. package/dist/custom/action-param-dialog.d.ts +0 -21
  69. package/dist/custom/export-progress-dialog.d.ts +0 -43
  70. package/dist/hooks/use-export-job.d.ts +0 -34
  71. package/dist/renderers/data-display/table.d.ts +0 -2
  72. package/dist/ui/chart.d.ts +0 -56
  73. package/dist/ui/toast.d.ts +0 -22
@@ -1,3 +1,4 @@
1
+ import { FilterBuilderCondition as AuthoredFilterBuilderCondition, FilterGroup as AuthoredFilterGroup } from '@object-ui/types';
1
2
  /**
2
3
  * ObjectUI
3
4
  * Copyright (c) 2024-present ObjectStack Inc.
@@ -16,21 +17,43 @@ import * as React from "react";
16
17
  * and `operator` here is the builder's own dropdown vocabulary
17
18
  * (`FILTER_BUILDER_OPERATORS`), not the spec's `$`-tokens.
18
19
  *
19
- * `@object-ui/types` already renamed its identical concept to
20
- * `FilterBuilderCondition` (objectui#3068); this is the same thing, so it takes
21
- * the same name rather than inventing a second dialect. Note the same name is
22
- * NOT the right answer everywhere: `@object-ui/app-shell`'s `FilterCondition`
23
- * really was the spec's ObjectQL AST and was re-exported (objectui#3169).
20
+ * DERIVED from `@object-ui/types`, never declared a second time (objectui#9306,
21
+ * which settles objectui#6349's two parked name-authority rows). That declaration is
22
+ * the one name authority for the row; this one takes every member from it
23
+ * (`id`, required since objectui#8415, and `field`) and restates exactly two,
24
+ * each a named extension:
25
+ *
26
+ * - `operator` — {@link FilterBuilderOperator}: the protocol's ids, which is
27
+ * what the authority declares, plus the opt-in `exists` / `notExists` the
28
+ * protocol has no member for (objectui#9559 ruling B keeps them opt-in and
29
+ * unfolded). It was `string`, which let a caller hand the builder an id no
30
+ * dropdown entry can hold.
31
+ * - `value` — REQUIRED and narrowed to the scalar-or-list union this builder
32
+ * edits, where the authority declares `value?: any`. Every exported helper
33
+ * below that takes a row's value (`reshapeFilterValue`, `retypeFilterValue`,
34
+ * `narrowFilterValueToOptions`, …) is typed on this member, so `any` here
35
+ * would switch their checking off in silence.
24
36
  */
25
- export interface FilterBuilderCondition {
26
- id: string;
27
- field: string;
28
- operator: string;
37
+ export interface FilterBuilderCondition extends Omit<AuthoredFilterBuilderCondition, "operator" | "value"> {
38
+ operator: FilterBuilderOperator;
29
39
  value: string | number | boolean | (string | number | boolean)[];
30
40
  }
31
- export interface FilterGroup {
32
- id: string;
33
- logic: "and" | "or";
41
+ /**
42
+ * The group the FilterBuilder edits: one `logic` over FLAT rows.
43
+ *
44
+ * DERIVED from `@object-ui/types` like the row above (objectui#9306). Only
45
+ * `conditions` is restated, to hold THIS package's row; `id` and `logic` are
46
+ * the authority's own. So `id` is OPTIONAL here, as it is there: nothing in
47
+ * this builder reads a group's `id`, `onChange` hands back whatever group the
48
+ * host passed (an id-less group comes back id-less), and only `EMPTY_GROUP`
49
+ * invents one. The `id: string` this package used to declare described a
50
+ * group the builder never guaranteed.
51
+ *
52
+ * `conditions` is flat because the builder is: it draws each entry as one row
53
+ * and has no control that creates a group. The authority retired nested
54
+ * sub-groups in the same change and its zod mirror refuses one by name.
55
+ */
56
+ export interface FilterGroup extends Omit<AuthoredFilterGroup, "conditions"> {
34
57
  conditions: FilterBuilderCondition[];
35
58
  }
36
59
  export interface FilterBuilderProps {
@@ -42,7 +65,7 @@ export interface FilterBuilderProps {
42
65
  value: string;
43
66
  label: string;
44
67
  }>;
45
- /** For lookup/master_detail/user/owner fields — referenced object name */
68
+ /** For lookup/master_detail/user fields — referenced object name */
46
69
  referenceTo?: string;
47
70
  /** Display field on the referenced object (defaults to "name") */
48
71
  displayField?: string;
@@ -53,36 +76,50 @@ export interface FilterBuilderProps {
53
76
  onChange?: (value: FilterGroup) => void;
54
77
  className?: string;
55
78
  showClearAll?: boolean;
79
+ /**
80
+ * Opt-in operator ids ({@link OPT_IN_OPERATORS}) this instance may offer, on
81
+ * top of the ones every FilterBuilder draws.
82
+ *
83
+ * The dropdown is shared by consumers that persist a filter into DIFFERENT
84
+ * dialects, and an operator one dialect can carry is not automatically
85
+ * storable in another — so whether an opt-in operator is offered is the
86
+ * consumer's answer, not this component's. See {@link OPT_IN_OPERATORS} for
87
+ * the measurement that made this a prop instead of one global list.
88
+ */
89
+ extraOperators?: readonly string[];
56
90
  }
57
91
  declare const defaultOperators: readonly [{
58
92
  readonly value: "equals";
59
93
  readonly label: "Equals";
60
94
  }, {
61
- readonly value: "notEquals";
95
+ readonly value: "not_equals";
62
96
  readonly label: "Does not equal";
63
97
  }, {
64
98
  readonly value: "contains";
65
99
  readonly label: "Contains";
66
100
  }, {
67
- readonly value: "notContains";
101
+ readonly value: "icontains";
102
+ readonly label: "Contains (ignore case)";
103
+ }, {
104
+ readonly value: "not_contains";
68
105
  readonly label: "Does not contain";
69
106
  }, {
70
- readonly value: "isEmpty";
107
+ readonly value: "is_empty";
71
108
  readonly label: "Is empty";
72
109
  }, {
73
- readonly value: "isNotEmpty";
110
+ readonly value: "is_not_empty";
74
111
  readonly label: "Is not empty";
75
112
  }, {
76
- readonly value: "greaterThan";
113
+ readonly value: "greater_than";
77
114
  readonly label: "Greater than";
78
115
  }, {
79
- readonly value: "lessThan";
116
+ readonly value: "less_than";
80
117
  readonly label: "Less than";
81
118
  }, {
82
- readonly value: "greaterOrEqual";
119
+ readonly value: "greater_than_or_equal";
83
120
  readonly label: "Greater than or equal";
84
121
  }, {
85
- readonly value: "lessOrEqual";
122
+ readonly value: "less_than_or_equal";
86
123
  readonly label: "Less than or equal";
87
124
  }, {
88
125
  readonly value: "before";
@@ -97,19 +134,19 @@ declare const defaultOperators: readonly [{
97
134
  readonly value: "in";
98
135
  readonly label: "In";
99
136
  }, {
100
- readonly value: "notIn";
137
+ readonly value: "not_in";
101
138
  readonly label: "Not in";
102
139
  }, {
103
- readonly value: "startsWith";
140
+ readonly value: "starts_with";
104
141
  readonly label: "Starts with";
105
142
  }, {
106
- readonly value: "endsWith";
143
+ readonly value: "ends_with";
107
144
  readonly label: "Ends with";
108
145
  }, {
109
- readonly value: "isNull";
146
+ readonly value: "is_null";
110
147
  readonly label: "Is null";
111
148
  }, {
112
- readonly value: "isNotNull";
149
+ readonly value: "is_not_null";
113
150
  readonly label: "Is not null";
114
151
  }, {
115
152
  readonly value: "exists";
@@ -120,19 +157,394 @@ declare const defaultOperators: readonly [{
120
157
  }];
121
158
  /**
122
159
  * The FilterBuilder's own operator vocabulary — every id its dropdown can
123
- * hold, derived from the operators it actually renders.
160
+ * hold, derived from the operators it actually renders. Includes the opt-in
161
+ * ids: they are drawable, just not offered unconditionally.
124
162
  *
125
- * Exported because several translation tables map an external vocabulary
126
- * (the spec's `VIEW_FILTER_OPERATORS`, Mongo `$`-tokens) *onto* this one, and
127
- * a value that is not in this list produces a condition row whose operator
128
- * select has nothing to select. Those tables assert against this export
129
- * rather than restating it, so adding an operator here is the only edit
130
- * needed to widen them.
163
+ * Since objectui#9306 these ARE the protocol's ids — the twenty members of the
164
+ * spec's `VIEW_FILTER_OPERATORS`, plus the opt-in `exists` / `notExists` the
165
+ * protocol has no member for — so a table that maps the spec's vocabulary onto
166
+ * this one is an identity over the twenty. It is still exported because those
167
+ * tables, and the parity pins over them, assert against this list rather than
168
+ * restating it: a value that is not in it produces a condition row whose
169
+ * operator select has nothing to select.
170
+ */
171
+ export declare const FILTER_BUILDER_OPERATORS: ("in" | "starts_with" | "ends_with" | "contains" | "equals" | "not_equals" | "greater_than" | "greater_than_or_equal" | "less_than" | "less_than_or_equal" | "before" | "after" | "not_in" | "not_contains" | "icontains" | "between" | "is_null" | "is_not_null" | "is_empty" | "is_not_empty" | "exists" | "notExists")[];
172
+ /**
173
+ * An operator id the FilterBuilder can render: the spec's `ViewFilterOperator`
174
+ * plus the two opt-in existence ids (objectui#9306). A type-level pin in
175
+ * `filter-builder-protocol-ids-9306.test.tsx` holds it EQUAL to that union, so
176
+ * a protocol member added upstream fails to compile there until the dropdown
177
+ * offers it.
131
178
  */
132
- export declare const FILTER_BUILDER_OPERATORS: ("endsWith" | "startsWith" | "in" | "after" | "equals" | "notEquals" | "contains" | "notContains" | "isEmpty" | "isNotEmpty" | "greaterThan" | "lessThan" | "greaterOrEqual" | "lessOrEqual" | "before" | "between" | "notIn" | "isNull" | "isNotNull" | "exists" | "notExists")[];
133
- /** An operator id the FilterBuilder can render. */
134
179
  export type FilterBuilderOperator = (typeof defaultOperators)[number]['value'];
135
- declare function FilterBuilder({ fields, value, onChange, className, showClearAll, }: FilterBuilderProps): React.JSX.Element;
180
+ /**
181
+ * The spelling a stored operator is READ as — the protocol id for every
182
+ * spelling the spec or this builder ever wrote (objectui#9306).
183
+ *
184
+ * The ruling this executes: camelCase is the deprecated alias form
185
+ * (objectui#7993), accepted on read and rewritten on write. So a filter stored
186
+ * as `{ operator: 'greaterOrEqual' }` keeps loading — it is folded here, through
187
+ * the spec's own `normalizeFilterOperator`, onto `greater_than_or_equal` — and
188
+ * the builder holds and emits that canonical id from then on, so the next write
189
+ * the author makes stores it. A lossless conversion on the read side, ⛔ not a
190
+ * second vocabulary: nothing this builder EMITS is ever a camelCase id.
191
+ *
192
+ * An id nothing folds (`exists`, `notExists`, a spelling from no vocabulary)
193
+ * comes back unchanged, exactly as the spec's normalizer returns it: inventing
194
+ * a mapping for a word the protocol does not contain would be a claim, not a
195
+ * repair.
196
+ *
197
+ * ⚠️ One guard on the spec's answer, and only because the builder now STORES
198
+ * what this returns (before objectui#9306 the fold was used for comparison
199
+ * only). `normalizeFilterOperator` is declared to return a string, but it
200
+ * looks the spelling up on a plain object, so an `Object.prototype` key comes
201
+ * back as whatever the prototype holds — `'constructor'` returns the `Object`
202
+ * function, measured on `@objectstack/spec` 17.4.0. Held in a row and written
203
+ * back, that would drop the row's operator on serialisation. A non-string
204
+ * answer is therefore treated as "nothing folds" and the spelling is kept;
205
+ * the defect itself belongs to the spec and is reported there, not patched
206
+ * around here in any other way.
207
+ */
208
+ export declare function normalizeFilterBuilderOperator(operator: string): string;
209
+ /**
210
+ * Operator ids — this dropdown's own ids, which are the protocol's canonical
211
+ * spellings since objectui#9306 — for which this builder renders no value
212
+ * input, so "no value" is the row's FINISHED state, not an unfinished one
213
+ * (objectui#4744).
214
+ *
215
+ * This is the source of truth for that distinction, and it lives here because
216
+ * this component is the thing that decides it. But membership here is not the
217
+ * whole question: `needsValueInput` below asks it of the row's spelling AFTER
218
+ * {@link normalizeFilterBuilderOperator} (objectui#9302), so the gate answers
219
+ * "no value" for spellings that are NOT members — the deprecated camelCase ids
220
+ * a stored filter may still carry, and the alias rows the spec publishes for
221
+ * those same operators. A consumer holding a spelling that did not come from
222
+ * this dropdown must therefore fold it before asking, and fold this set's own
223
+ * members through the same normalizer too — the spec's `normalizeFilterOperator`
224
+ * is the identity on them, so that fold costs nothing and keeps the consumer
225
+ * correct whichever vocabulary this set is written in. How much wider the
226
+ * gate's preimage is is NOT restated here: the pin
227
+ * `filter-builder-valueless-canonical-spelling-9302.test.tsx` walks the
228
+ * spec's two published tables and names every row it measures, which is the
229
+ * only form of that answer that moves when those tables do (AGENTS.md #9).
230
+ *
231
+ * Every consumer that has to tell a complete value-less row from a half-filled
232
+ * one reads the membership FROM here rather than restating it:
233
+ *
234
+ * - `plugin-list`'s `convertFilterGroupToAST` — what the live grid QUERIES;
235
+ * - `app-shell`'s `foldFilterGroupToSpecRules` — what a saved view PERSISTS.
236
+ *
237
+ * Placement is forced by the dependency graph — `app-shell` depends on
238
+ * `plugin-list` depends on this package, so this is the only module all three
239
+ * can reach — and it is also where the set belongs on the merits: the fact it
240
+ * states is a fact about what this dropdown draws.
241
+ *
242
+ * Why it exists at all: each consumer used to keep its own copy, and the
243
+ * live-grid copy listed only the two empty-string operators. A fresh row is
244
+ * seeded `{ operator: 'equals', value: '' }` and the operator dropdown
245
+ * preserves `value`, so picking **Is null** as the first action left
246
+ * `value: ''` — the grid read that as an unfinished row, dropped it, and
247
+ * applied NO filter at all while the panel showed one. Silent, and every
248
+ * record came back.
249
+ *
250
+ * `exists` / `notExists` stay listed even though `OPT_IN_OPERATORS` withholds
251
+ * them from most consumers (objectui#4736): the builder still draws them
252
+ * value-less for `FilterConditionField`, which is exactly what this set
253
+ * describes. Whether a consumer can EXPRESS an operator is a separate
254
+ * question, answered by `OPT_IN_OPERATORS` and by each consumer's parity test.
255
+ */
256
+ export declare const VALUELESS_FILTER_BUILDER_OPERATORS: ReadonlySet<string>;
257
+ /**
258
+ * The SHAPE an operator's `value` must have — the question
259
+ * `ViewFilterRuleSchema` asks, asked here in the one place that decides what
260
+ * input the user is given (objectui#3958).
261
+ *
262
+ * - `list` — membership over any arity, `[]` included (`in` / `not_in`).
263
+ * - `pair` — exactly two bounds, in order (`between`).
264
+ * - `scalar` — everything else, including the value-less operators above
265
+ * (they draw no input at all, so their shape never reaches storage).
266
+ */
267
+ export type FilterValueArity = "scalar" | "list" | "pair";
268
+ /**
269
+ * Which value shape `operator` takes, whichever spelling it arrives in.
270
+ *
271
+ * The argument is folded through {@link normalizeFilterBuilderOperator} — the
272
+ * spec's own `normalizeFilterOperator` plus the builder's one unfolded legacy
273
+ * id — first, so the canonical id this dropdown emits (`not_in`) and the
274
+ * deprecated alias a stored filter may still carry (`notIn`) land on the same
275
+ * answer. That fold is the whole point: one vocabulary, declared upstream,
276
+ * consulted here — not a second local dialect kept in sync by hand.
277
+ *
278
+ * @internal exported for tests
279
+ */
280
+ export declare function filterValueArity(operator: string): FilterValueArity;
281
+ /**
282
+ * Re-shape a row's `value` for the operator it is being changed TO.
283
+ *
284
+ * Switching the operator dropdown used to update `operator` alone, so the
285
+ * value the previous family had produced simply survived the switch — a scalar
286
+ * `"acme"` (or the seed `""`) still sitting there under `in`, which is
287
+ * precisely the shape `ViewFilterRuleSchema` refuses. Re-shaping rather than
288
+ * blanking keeps what the user typed wherever it still means something:
289
+ *
290
+ * - to `list`: a scalar becomes a one-element list (`"acme"` → `["acme"]`),
291
+ * an empty scalar becomes `[]` — a real, spec-valid predicate;
292
+ * - to `pair`: the first two entries become `[min, max]`, padded with `""`;
293
+ * with nothing to carry it collapses to `[]`, which the write path reads
294
+ * as "row not filled in yet" and drops, exactly as it drops an empty
295
+ * scalar today;
296
+ * - to `scalar`: the first entry of a list survives; the rest cannot, and
297
+ * dropping them is the honest answer — `equals` compares against one value.
298
+ *
299
+ * @internal exported for tests
300
+ */
301
+ export declare function reshapeFilterValue(value: FilterBuilderCondition["value"], nextOperator: string): FilterBuilderCondition["value"];
302
+ /**
303
+ * The operator a row keeps after its FIELD is changed, given the operators the
304
+ * NEW field offers (objectui#4768).
305
+ *
306
+ * Each field type has its own operator bucket, and the buckets are not nested:
307
+ * a `select` column offers `in` / `not_in`, a `text` column does not. Changing
308
+ * the field used to write `{ field }` alone, so the row's operator survived
309
+ * into a bucket that no longer contains it — `in` on a text column. The Radix
310
+ * trigger then had nothing to render (its `SelectValue` matches against the
311
+ * `SelectItem`s actually mounted) and went BLANK, while the row went on
312
+ * filtering by an operator the user could no longer see or reach.
313
+ *
314
+ * So the two edits are one edit, exactly as `changeOperator` made the operator
315
+ * and its value shape one edit: an operator the new bucket still offers is the
316
+ * user's choice and is KEPT; one it does not is replaced by the bucket's first
317
+ * entry (`equals` for every bucket this builder draws), and the caller then
318
+ * re-shapes the value for that operator's family.
319
+ *
320
+ * Membership is decided CANONICALLY, through
321
+ * {@link normalizeFilterBuilderOperator} — the same fold `filterValueArity`
322
+ * uses. A caller can hand this helper a row spelled with the deprecated alias
323
+ * `notIn` while the dropdown lists the protocol id `not_in`; those are one
324
+ * operator, so a select→lookup switch must not silently rewrite the author's
325
+ * operator to `equals` just because the two spellings differ. The fold is safe
326
+ * to compare through because it is INJECTIVE over this builder's vocabulary —
327
+ * all 22 ids in `defaultOperators` normalize to 22 distinct canonical
328
+ * operators, pinned in `filter-builder-field-switch-operator.test.tsx` — so no
329
+ * two OFFERED operators can ever collapse onto one another.
330
+ *
331
+ * @internal exported for tests
332
+ */
333
+ export declare function reconcileOperatorForField(operator: string, offeredOperators: ReadonlyArray<{
334
+ value: string;
335
+ }>): string;
336
+ /**
337
+ * The offered id the operator trigger COMPARES against — the last site in this
338
+ * component that read the operator literally instead of through the spec's
339
+ * fold (objectui#7561).
340
+ *
341
+ * Radix matches `SelectValue` against the `SelectItem`s actually MOUNTED, and
342
+ * the mounted ids are this builder's own vocabulary — the protocol's canonical
343
+ * ids since objectui#9306 (they were camelCase when this helper was written). A
344
+ * row can hold the operator under another spelling of the SAME operator and
345
+ * still be perfectly valid:
346
+ *
347
+ * - a deprecated camelCase alias such as `greaterThan`, which a filter saved
348
+ * before objectui#9306 still carries;
349
+ * - the spec's alias table `gt` / `lt` / `eq`, which three schema-catalog
350
+ * entries author today.
351
+ *
352
+ * When this helper was written the mounted id was `greaterThan` and the stored
353
+ * spelling `greater_than`; neither matched the other literally, so the trigger
354
+ * drew BLANK over a row that filtered correctly — the user's own operator,
355
+ * invisible and unreachable.
356
+ * Everywhere the operator's MEANING matters this component already folds first
357
+ * ({@link filterValueArity}, {@link reconcileOperatorForField}); this was the
358
+ * one place it did not, and that omission — not the vocabulary divergence — is
359
+ * what produced the blank.
360
+ *
361
+ * So the comparison is made canonically and the MOUNTED spelling is handed to
362
+ * the trigger. Three things this deliberately does NOT do, each of them a
363
+ * separate ruling (objectui#7561):
364
+ *
365
+ * 1. it does not rewrite `condition.operator` — the row keeps the spelling it
366
+ * arrived with, exactly as {@link reconcileOperatorForField} keeps it, and
367
+ * nothing is written back on render. (Since objectui#9306 the BUILDER
368
+ * folds a row's spelling once, at its read boundary — see
369
+ * {@link normalizeFilterBuilderOperator} — so the rows it renders already
370
+ * hold protocol ids. This helper still folds both sides, because it is
371
+ * exported and its caller's rows need not have come through that
372
+ * boundary.)
373
+ * 2. it does not mount a new `SelectItem`, so the vocabulary the dropdown
374
+ * EMITS is byte-identical — contrast the value select above, where
375
+ * objectui#4874 ruling C mounts the outside-options value as its own
376
+ * option. That answer is right there and wrong here: the value's domain is
377
+ * the author's data, while the operator's domain is a declared vocabulary,
378
+ * and mounting a foreign spelling would admit a second one;
379
+ * 3. it does not widen what any schema ACCEPTS.
380
+ *
381
+ * Safe to compare through because the fold is INJECTIVE over this builder's
382
+ * vocabulary — all 22 ids in `defaultOperators` normalize to 22 distinct
383
+ * canonical operators, pinned in `filter-builder-field-switch-operator.test.tsx`
384
+ * — so no two OFFERED operators can collapse onto one another and the match is
385
+ * unambiguous. An operator no offered id folds onto falls through to the row's
386
+ * own spelling and still draws blank: inventing a label for a word this
387
+ * vocabulary does not contain would be a claim, not a repair.
388
+ *
389
+ * @internal exported for tests
390
+ */
391
+ export declare function mountedOperatorValue(operator: string, offeredOperators: ReadonlyArray<{
392
+ value: string;
393
+ }>): string;
394
+ /**
395
+ * Re-TYPE a row's `value` for the field it is being changed TO — the type
396
+ * question, standing beside {@link reshapeFilterValue}'s shape question
397
+ * (objectui#4781).
398
+ *
399
+ * Changing a row's field used to carry the value through untouched whenever the
400
+ * shape did not have to change, which is right about the shape and wrong about
401
+ * the type: the value input is re-drawn from the NEW field's type, and an
402
+ * `<input type="number">` renders a non-numeric value as BLANK. So a `text`
403
+ * row filtered `equals "acme"`, pointed at a number column, showed an empty box
404
+ * while the row went on carrying `"acme"` — `foldFilterGroupToSpecRules`
405
+ * persisted it and the live grid queried `amount equals "acme"`. The same
406
+ * invisible-value shape objectui#4768 closed on the operator, one column over.
407
+ *
408
+ * Carry-if-possible, exactly as `reshapeFilterValue` carries what it can across
409
+ * a family change: `"42"` moved to a number column becomes `42` and survives;
410
+ * `"acme"` cannot be read as a number by any clean reading, so it clears to the
411
+ * family's empty shape — scalar `""`, list `[]`, pair `[]`. What a value can
412
+ * become is decided in one place, {@link convertScalarToFamily}.
413
+ *
414
+ * The arity is read from the operator through the same `filterValueArity` fold
415
+ * the rest of this component uses, because each shape clears differently:
416
+ *
417
+ * - `scalar` — converted, or `""`.
418
+ * - `list` — converted ENTRY BY ENTRY, keeping the ones that carry:
419
+ * `["42", "acme"]` → `[42]`, and `[]` when none do. Reachable today only
420
+ * between the two buckets that offer `in`/`not_in` (`select` ↔ `lookup`),
421
+ * which are both text-family, so the conversion is the identity there; it
422
+ * is written for the family the operator lands in rather than for today's
423
+ * buckets, and pinned directly on this helper.
424
+ * - `pair` — both bounds converted independently, an unconvertible bound
425
+ * becoming `""` so the range keeps its two slots; both empty collapses to
426
+ * `[]`, the "not filled in yet" shape `reshapeFilterValue` also produces
427
+ * and the write path drops.
428
+ *
429
+ * @internal exported for tests
430
+ */
431
+ export declare function retypeFilterValue(value: FilterBuilderCondition["value"], fieldType: string | undefined, operator: string): FilterBuilderCondition["value"];
432
+ /** The shape of a `fields` entry that the option-domain helpers below read. */
433
+ type OptionBearingField = {
434
+ type?: string;
435
+ options?: ReadonlyArray<{
436
+ value: string;
437
+ label: string;
438
+ }>;
439
+ };
440
+ /**
441
+ * Narrow a row's `value` to the option DOMAIN of the field it is being changed
442
+ * TO — the value-domain question, standing beside {@link retypeFilterValue}'s
443
+ * type question and {@link reshapeFilterValue}'s shape question
444
+ * (objectui#4874).
445
+ *
446
+ * objectui#4781 settled the TYPE half and deliberately stopped there: `select`
447
+ * and `lookup` are text-family, so `"acme"` is a value those columns can
448
+ * perfectly well HOLD. What it cannot be is one of the column's options — and
449
+ * the control drawn for those columns is a Radix Select, whose trigger renders
450
+ * only what one of its mounted `SelectItem`s carries. So a text row filtered
451
+ * `equals "acme"`, pointed at a picklist column of `won`/`lost`, showed a BLANK
452
+ * trigger while the row went on carrying `"acme"`:
453
+ * `foldFilterGroupToSpecRules` persisted it and the live grid queried
454
+ * `stage equals "acme"`. The third face of the same invisible value —
455
+ * objectui#4768 on the operator, objectui#4781 on the value's type, this one on
456
+ * its domain.
457
+ *
458
+ * Ruled 2026-08-17 as **A + C combined**, and the A half is this function:
459
+ * clear on non-membership, but ONLY where the option set is the column's whole
460
+ * domain ({@link hasStaticOptionDomain}). A remote/async column's value is
461
+ * returned untouched — clearing it would delete a perfectly valid lookup id on
462
+ * the strength of a local page that never claimed to be complete, and the C
463
+ * half (the render path's temporary option) is what keeps it visible instead.
464
+ * Option B — keep the value and tolerate it being invisible — stays rejected,
465
+ * as it was on objectui#4781.
466
+ *
467
+ * The empty shape a cleared value falls to is objectui#4781's, unchanged and
468
+ * read from the same `filterValueArity` fold:
469
+ *
470
+ * - `scalar` — `""`.
471
+ * - `list` — filtered ENTRY BY ENTRY, keeping the members: `["acme", "won"]`
472
+ * → `["won"]`, and `[]` when none survive. Per-entry rather than
473
+ * all-or-nothing because each entry is its own filter term, and one bad
474
+ * term is no reason to throw away the user's good ones.
475
+ * - `pair` — untouched; `between` is not option-driven.
476
+ *
477
+ * @internal exported for tests
478
+ */
479
+ export declare function narrowFilterValueToOptions(value: FilterBuilderCondition["value"], field: OptionBearingField | undefined, operator: string): FilterBuilderCondition["value"];
480
+ /**
481
+ * Is this row's value FINISHED for the operator it carries — the arity-aware
482
+ * reading of "the user has filled this in" (objectstack#8815).
483
+ *
484
+ * Both write paths used to ask this with one shape-blind predicate:
485
+ *
486
+ * ```ts
487
+ * value == null || value === '' || (Array.isArray(value) && value.length === 0)
488
+ * ```
489
+ *
490
+ * which is right for `scalar` and `list` and WRONG for `pair`. A `between` row
491
+ * with one bound typed is `["2024-01-01", ""]` — an array of length 2, so it
492
+ * read as complete, and both consumers carried it through. The live grid then
493
+ * queried a range with an empty upper bound and the server refused the whole
494
+ * view (`400 INVALID_FILTER`), which is the "该视图的查询被拒绝" the reporter
495
+ * hit; the saved-view path persisted the same half-range, so the refusal came
496
+ * back on every later read of that view.
497
+ *
498
+ * The spec cannot catch this on the way past — measured against
499
+ * `ViewFilterRuleSchema` on `@objectstack/spec` 17.0.0: `["2024-01-01", ""]` is
500
+ * ACCEPTED (it counts the two slots, not what is in them), while a scalar or a
501
+ * one-element array is refused. So the empty bound sails through authoring
502
+ * validation and only dies at query time, one layer too late to be actionable.
503
+ * That makes it the PRODUCER's job to not emit it — this component is what
504
+ * decides when a row is finished, exactly as it already decides which operators
505
+ * finish with no value at all ({@link VALUELESS_FILTER_BUILDER_OPERATORS}), and
506
+ * for the same reason: each consumer keeping its own copy is how the two came to
507
+ * disagree in the first place.
508
+ *
509
+ * The arity is read through the same `filterValueArity` fold the rest of this
510
+ * file uses, so a stored rule spelled canonically gets the same answer as the
511
+ * dropdown's own id.
512
+ *
513
+ * What each shape needs:
514
+ *
515
+ * - `pair` — BOTH bounds present. A range missing an end is not a narrower
516
+ * range, it is a query the server refuses.
517
+ * - `list` — at least one entry; `[]` is the "nothing chosen yet" shape.
518
+ * - `scalar` — anything that is not the unfilled shape.
519
+ *
520
+ * "Present" is {@link isValueUnset}, never `!bound` (objectui#4873): `0` is a
521
+ * real bound on a number column and `false` is a real value, and reading either
522
+ * as unfilled would drop a filter the user can see on screen.
523
+ *
524
+ * This answers the VALUE question only. Whether an operator wants a value at all
525
+ * stays with each caller, because the two layers see different vocabularies:
526
+ * `app-shell` also has to recognise the canonical spellings (`is_null`) that
527
+ * never reach this dropdown.
528
+ *
529
+ * @internal exported for tests
530
+ */
531
+ export declare function isFilterValueComplete(operator: string, value: FilterBuilderCondition["value"] | undefined | null): boolean;
532
+ /**
533
+ * The operators the dropdown offers for a field of `fieldType`, given the
534
+ * opt-in ids this instance was granted.
535
+ *
536
+ * A pure function rather than a closure so the selection rule — and above all
537
+ * the {@link OPT_IN_OPERATORS} gate, whose whole job is to keep an operator OFF
538
+ * the surfaces that cannot store it — is assertable without driving a Radix
539
+ * popover open in jsdom.
540
+ *
541
+ * @internal exported for tests
542
+ */
543
+ export declare function operatorsForFieldType(fieldType: string | undefined, extraOperators?: readonly string[]): ReadonlyArray<{
544
+ value: string;
545
+ label: string;
546
+ }>;
547
+ declare function FilterBuilder({ fields, value, onChange, className, showClearAll, extraOperators, }: FilterBuilderProps): React.JSX.Element;
136
548
  declare namespace FilterBuilder {
137
549
  var displayName: string;
138
550
  }
@@ -1,4 +1,29 @@
1
1
  import { default as React } from 'react';
2
+ /**
3
+ * The accessibility attributes the dialog's injected editor MUST carry, handed
4
+ * to `children` as its required fourth argument (objectui#4824 / #4832).
5
+ *
6
+ * Shaped as a spreadable set of real DOM attributes rather than the ids behind
7
+ * them, on purpose: `{...aria}` is the whole integration, so a host cannot get
8
+ * the composition subtly wrong, cannot name a node outside the dialog, and
9
+ * cannot compute its own `aria-invalid` from a prop it forgot to plumb — the
10
+ * three ways every one of the three surfaces got this wrong before.
11
+ */
12
+ export interface FullscreenEditorAria {
13
+ /**
14
+ * The dialog title's text node — the field label (#3393) reused as the
15
+ * control's accessible name rather than a second, string-valued author for it.
16
+ */
17
+ 'aria-labelledby': string;
18
+ /** Mirrors `error`, so the dialog cannot disagree with the inline control. */
19
+ 'aria-invalid': boolean;
20
+ /**
21
+ * The dialog-local message node. A SINGLE id, present only when `error` is —
22
+ * `aria-errormessage` is exposed only alongside `aria-invalid="true"`, and
23
+ * this repo's toolchain does not accept an id list here.
24
+ */
25
+ 'aria-errormessage'?: string;
26
+ }
2
27
  export interface FullscreenEditorProps {
3
28
  /**
4
29
  * The committed value. Seeds the draft each time the dialog opens — read at
@@ -39,6 +64,22 @@ export interface FullscreenEditorProps {
39
64
  * widgets and applies it on the built-in branch.
40
65
  */
41
66
  disabled?: boolean;
67
+ /**
68
+ * The field's validation message, or `undefined` when the field is valid.
69
+ *
70
+ * **Required, not optional** (objectui#4824). Every consumer already has it —
71
+ * the registered widgets take `error` off the widget props contract
72
+ * (objectui#3222), the built-in branch reads `fieldState.error?.message` — and
73
+ * an omitted `error` reproduces this issue exactly: a dialog that announces
74
+ * `aria-invalid="false"` while the same field's inline control announces
75
+ * `true`. A key the type system makes every call site answer cannot be
76
+ * forgotten by the next surface; an optional one was, three times.
77
+ *
78
+ * **Producers**: `TextAreaField`, `RichTextField`, and the form renderer's
79
+ * built-in `textarea` branch (which reads it off the PRE-strip props, exactly
80
+ * like `label` — `stripRendererOnlyProps` discards `error` for the DOM).
81
+ */
82
+ error: string | undefined;
42
83
  /**
43
84
  * The editor itself, rendered inside the dialog body against the draft. The
44
85
  * host passes the SAME editor it renders inline.
@@ -48,9 +89,14 @@ export interface FullscreenEditorProps {
48
89
  * editor's disabled state belongs on. Ignoring it is not a write-back hole —
49
90
  * `onCommit` is gated here regardless — but it does leave a control that looks
50
91
  * editable while the field is not, so every in-repo host applies it.
92
+ *
93
+ * The fourth argument is the accessibility set this component owns — name and
94
+ * validation state — and it is spread onto the control the host renders. See
95
+ * `FullscreenEditorAria` and the section above for why it is computed here
96
+ * rather than by each host.
51
97
  */
52
- children: (draft: string, setDraft: (next: string) => void, disabled: boolean) => React.ReactNode;
98
+ children: (draft: string, setDraft: (next: string) => void, disabled: boolean, aria: FullscreenEditorAria) => React.ReactNode;
53
99
  /** Optional footer status for the draft (e.g. a character counter). */
54
100
  footer?: (draft: string) => React.ReactNode;
55
101
  }
56
- export declare function FullscreenEditor({ value, onCommit, label, testIdPrefix, readOnly, disabled, children, footer, }: FullscreenEditorProps): React.JSX.Element | null;
102
+ export declare function FullscreenEditor({ value, onCommit, label, testIdPrefix, readOnly, disabled, error, children, footer, }: FullscreenEditorProps): React.JSX.Element | null;