@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.
- package/CHANGELOG.md +10155 -0
- package/README.md +20 -10
- package/dist/custom/button-group.d.ts +2 -2
- package/dist/custom/character-count.d.ts +56 -0
- package/dist/custom/combobox.d.ts +2 -4
- package/dist/custom/config-field-renderer.d.ts +1 -1
- package/dist/custom/config-panel-renderer.d.ts +3 -3
- package/dist/custom/empty.d.ts +16 -4
- package/dist/custom/field.d.ts +1 -1
- package/dist/custom/filter-builder.d.ts +448 -36
- package/dist/custom/fullscreen-editor.d.ts +48 -2
- package/dist/custom/index.d.ts +27 -28
- package/dist/custom/item.d.ts +2 -2
- package/dist/custom/kbd.d.ts +1 -1
- package/dist/custom/mobile-dialog-content.d.ts +22 -1
- package/dist/custom/navigation-overlay.d.ts +105 -0
- package/dist/custom/refresh-indicator.d.ts +8 -2
- package/dist/custom/resizable.d.ts +1 -1
- package/dist/custom/view-states.d.ts +74 -1
- package/dist/debug/index.d.ts +2 -2
- package/dist/hooks/related-count-store.d.ts +26 -28
- package/dist/index.css +174 -364
- package/dist/index.d.ts +21 -21
- package/dist/index.js +7274 -6350
- package/dist/index.umd.cjs +12 -13
- package/dist/lib/close-label.d.ts +1 -2
- package/dist/lib/date-fns-locale.d.ts +41 -0
- package/dist/lib/form-control-dom-props.d.ts +77 -0
- package/dist/lib/lazy-icon.d.ts +32 -1
- package/dist/lib/lucide-record-icon-names.d.ts +30 -0
- package/dist/lib/slider-thumb.d.ts +86 -0
- package/dist/lib/utils.d.ts +113 -0
- package/dist/notifications/index.d.ts +5 -5
- package/dist/renderers/action/action-bar.d.ts +8 -36
- package/dist/renderers/action/action-button.d.ts +15 -4
- package/dist/renderers/action/action-group.d.ts +5 -4
- package/dist/renderers/action/action-icon.d.ts +12 -2
- package/dist/renderers/action/action-menu.d.ts +5 -2
- package/dist/renderers/action/auto-trigger.d.ts +19 -4
- package/dist/renderers/action/resolve-icon.d.ts +27 -2
- package/dist/renderers/action/static-params.d.ts +79 -0
- package/dist/renderers/action/visibility-gate.d.ts +11 -3
- package/dist/renderers/basic/elements.d.ts +6 -3
- package/dist/renderers/basic/html-elements.d.ts +8 -4
- package/dist/renderers/basic/readProps.d.ts +60 -0
- package/dist/renderers/complex/data-table.d.ts +31 -14
- package/dist/renderers/complex/dataTableBindDiagnostic.d.ts +187 -0
- package/dist/renderers/form/validationRuleKeys.d.ts +45 -0
- package/dist/renderers/layout/box.d.ts +8 -0
- package/dist/renderers/layout/containers.d.ts +1 -10
- package/dist/renderers/layout/react-page.d.ts +16 -2
- package/dist/renderers/navigation/sidebar.d.ts +31 -0
- package/dist/share/index.d.ts +2 -2
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/button.d.ts +1 -1
- package/dist/ui/calendar.d.ts +3 -2
- package/dist/ui/carousel.d.ts +2 -2
- package/dist/ui/command.d.ts +8 -8
- package/dist/ui/form.d.ts +1 -1
- package/dist/ui/index.d.ts +46 -47
- package/dist/ui/input-otp.d.ts +2 -2
- package/dist/ui/pagination.d.ts +1 -1
- package/dist/ui/sheet.d.ts +1 -1
- package/dist/ui/sidebar.d.ts +4 -3
- package/dist/ui/slider.d.ts +2 -1
- package/package.json +17 -18
- package/dist/SchemaRenderer.d.ts +0 -4
- package/dist/custom/action-param-dialog.d.ts +0 -21
- package/dist/custom/export-progress-dialog.d.ts +0 -43
- package/dist/hooks/use-export-job.d.ts +0 -34
- package/dist/renderers/data-display/table.d.ts +0 -2
- package/dist/ui/chart.d.ts +0 -56
- 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
|
|
20
|
-
*
|
|
21
|
-
* the
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
107
|
+
readonly value: "is_empty";
|
|
71
108
|
readonly label: "Is empty";
|
|
72
109
|
}, {
|
|
73
|
-
readonly value: "
|
|
110
|
+
readonly value: "is_not_empty";
|
|
74
111
|
readonly label: "Is not empty";
|
|
75
112
|
}, {
|
|
76
|
-
readonly value: "
|
|
113
|
+
readonly value: "greater_than";
|
|
77
114
|
readonly label: "Greater than";
|
|
78
115
|
}, {
|
|
79
|
-
readonly value: "
|
|
116
|
+
readonly value: "less_than";
|
|
80
117
|
readonly label: "Less than";
|
|
81
118
|
}, {
|
|
82
|
-
readonly value: "
|
|
119
|
+
readonly value: "greater_than_or_equal";
|
|
83
120
|
readonly label: "Greater than or equal";
|
|
84
121
|
}, {
|
|
85
|
-
readonly value: "
|
|
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: "
|
|
137
|
+
readonly value: "not_in";
|
|
101
138
|
readonly label: "Not in";
|
|
102
139
|
}, {
|
|
103
|
-
readonly value: "
|
|
140
|
+
readonly value: "starts_with";
|
|
104
141
|
readonly label: "Starts with";
|
|
105
142
|
}, {
|
|
106
|
-
readonly value: "
|
|
143
|
+
readonly value: "ends_with";
|
|
107
144
|
readonly label: "Ends with";
|
|
108
145
|
}, {
|
|
109
|
-
readonly value: "
|
|
146
|
+
readonly value: "is_null";
|
|
110
147
|
readonly label: "Is null";
|
|
111
148
|
}, {
|
|
112
|
-
readonly value: "
|
|
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
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
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
|
-
|
|
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;
|