@object-ui/components 17.5.0 → 17.6.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 +1405 -0
- package/README.md +15 -7
- package/dist/custom/button-group.d.ts +1 -1
- package/dist/custom/character-count.d.ts +56 -0
- package/dist/custom/config-field-renderer.d.ts +1 -1
- package/dist/custom/config-panel-renderer.d.ts +3 -3
- package/dist/custom/export-progress-dialog.d.ts +1 -1
- package/dist/custom/filter-builder.d.ts +284 -4
- package/dist/custom/fullscreen-editor.d.ts +48 -2
- package/dist/custom/index.d.ts +28 -28
- package/dist/custom/item.d.ts +1 -1
- package/dist/custom/resizable.d.ts +1 -1
- package/dist/debug/index.d.ts +2 -2
- package/dist/index.css +22 -6
- package/dist/index.d.ts +21 -21
- package/dist/index.js +3958 -3650
- package/dist/index.umd.cjs +10 -6
- package/dist/lib/form-control-dom-props.d.ts +77 -0
- package/dist/lib/slider-thumb.d.ts +86 -0
- package/dist/notifications/index.d.ts +5 -5
- package/dist/renderers/basic/elements.d.ts +3 -3
- package/dist/renderers/basic/html-elements.d.ts +1 -1
- package/dist/renderers/form/validationRuleKeys.d.ts +45 -0
- package/dist/renderers/layout/react-page.d.ts +12 -2
- package/dist/share/index.d.ts +2 -2
- package/dist/ui/calendar.d.ts +1 -1
- package/dist/ui/carousel.d.ts +2 -2
- package/dist/ui/command.d.ts +2 -2
- package/dist/ui/form.d.ts +1 -1
- package/dist/ui/index.d.ts +47 -47
- package/dist/ui/pagination.d.ts +1 -1
- package/dist/ui/sidebar.d.ts +2 -2
- package/dist/ui/slider.d.ts +2 -1
- package/package.json +12 -12
- package/dist/custom/action-param-dialog.d.ts +0 -21
- package/dist/renderers/data-display/table.d.ts +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1410 @@
|
|
|
1
1
|
# @object-ui/components
|
|
2
2
|
|
|
3
|
+
## 17.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 279fb13: `ComponentInput.type` can declare a UNION, so a block stops warning about legal
|
|
8
|
+
writes its own description recommends
|
|
9
|
+
|
|
10
|
+
A registration's `type` was one coarse control kind, while a good number of spec
|
|
11
|
+
keys accept more than one shape. A declaration therefore had to pick an arm, and
|
|
12
|
+
the repo's own manifest gate then reported `type-mismatch` on the other arm's
|
|
13
|
+
legal values. Four of the five measured cases were the loud shape: the input's
|
|
14
|
+
`description` teaches the author to write an inline translation map
|
|
15
|
+
(`{ en, "zh-CN", … }`) while the same input's `type: 'string'` made
|
|
16
|
+
`sdui-parser`'s `checkType` warn about exactly that map — one platform authority
|
|
17
|
+
contradicting itself on the write it had just recommended. Because these land at
|
|
18
|
+
warning severity the page still compiled and rendered; the cost is that noise on
|
|
19
|
+
correct authoring trains authors, AI authors included, to dismiss the
|
|
20
|
+
`unknown-prop` and `type-mismatch` reports that are real.
|
|
21
|
+
|
|
22
|
+
`type` now accepts an ARRAY of coarse kinds as well as a single one (maintainer
|
|
23
|
+
ruling on objectui#3832, direction (a)), and a value passes the coarse check when
|
|
24
|
+
ANY declared arm accepts it. Both declaration sites in `@object-ui/types` move
|
|
25
|
+
together with the registry's own copy in `@object-ui/core`, and
|
|
26
|
+
`ComponentInputSchema` enforces the same widening — a non-empty array of
|
|
27
|
+
DISTINCT kinds, so an empty arm list or a repeated arm is refused at authoring
|
|
28
|
+
time rather than normalized behind the author's back.
|
|
29
|
+
|
|
30
|
+
Five declarations now spell their real contract, and the `type-mismatch` warning
|
|
31
|
+
on each of these legal writes is gone:
|
|
32
|
+
|
|
33
|
+
- `page:header.title`, `page:header.subtitle`, `page:card.title` —
|
|
34
|
+
string **or** inline translation map (the spec's union, measured against
|
|
35
|
+
`ComponentPropsMap` at the pinned rc.6; the renderers resolve both through
|
|
36
|
+
`pickLocalized`);
|
|
37
|
+
- `record:alert.title`, `record:alert.body` — the same two shapes, justified
|
|
38
|
+
against the RENDERER since the pinned spec carries no `record:alert` props
|
|
39
|
+
schema;
|
|
40
|
+
- `element:text_input.defaultValue` — `string | number`, the spec's union,
|
|
41
|
+
which had been narrowed to `'string'` with the number arm named only in prose.
|
|
42
|
+
|
|
43
|
+
**Backward compatible, and measured as such.** The single-kind form stays valid
|
|
44
|
+
and is still the canonical spelling for a one-arm key: it validates identically
|
|
45
|
+
(the diagnostics for one arm, `invalid-enum` and its `error` severity included,
|
|
46
|
+
are byte-identical), and `manifestFromConfigs` collapses a one-element array back
|
|
47
|
+
to the bare string, so every entry already in a published `sdui.manifest.json`
|
|
48
|
+
serializes unchanged and arrays appear only where a union was really declared.
|
|
49
|
+
The JSX authoring surface follows in the same step — `generateDts` emits a
|
|
50
|
+
TypeScript union for a union input, so the `.d.ts` an author type-checks against
|
|
51
|
+
accepts exactly what the gate accepts.
|
|
52
|
+
|
|
53
|
+
A union widens what is legal; it does not switch the check off. A value matching
|
|
54
|
+
NO declared arm is still reported, a multi-arm mismatch reports at its strictest
|
|
55
|
+
arm's severity (`error` when an `enum` arm is present, so an enum's closed list
|
|
56
|
+
does not become dismissible by having a second arm added next to it), and arms
|
|
57
|
+
are meant to match the contract rather than relax the gate:
|
|
58
|
+
`element:text_input.defaultValue` deliberately gains no `object` arm because the
|
|
59
|
+
spec rejects a map there, and `element:record_picker.emptyText` keeps its single
|
|
60
|
+
`'string'` arm because that renderer drops the map form (objectui#4163) — an arm
|
|
61
|
+
the renderer never honours would advertise a shape that cannot reach the screen.
|
|
62
|
+
- 8b9dc62: `element:text.content` and `element:button.label` declare the inline translation
|
|
63
|
+
map they already accept
|
|
64
|
+
|
|
65
|
+
Two more instances of the contradiction objectui#3832 fixed the mechanism for,
|
|
66
|
+
measured after that ruling had fixed its scope at five specimens and filed
|
|
67
|
+
separately as objectui#4970. Both inputs' own `description` tells the author to
|
|
68
|
+
write an inline translation map (`{ en, "zh-CN", … }`), both renderers resolve one
|
|
69
|
+
through `pickLocalized`, and both spec props schemas accept one — while the
|
|
70
|
+
declaration said `type: 'string'`, so the manifest gate reported
|
|
71
|
+
`type-mismatch` on the exact shape the block had recommended. Both blocks are in
|
|
72
|
+
`PUBLIC_BLOCKS`, so this reached authors through `sdui.manifest.json` and
|
|
73
|
+
`sdui-intrinsics.d.ts` as well as the save gate.
|
|
74
|
+
|
|
75
|
+
Each declaration is now `type: ['string', 'object']`, the union form
|
|
76
|
+
objectui#3832 introduced, and the arms are the ones the contract accepts —
|
|
77
|
+
re-measured on the `@objectstack/spec` 17.0.0 GA pin rather than carried over
|
|
78
|
+
from the issue, which was written at the 17.0.0-rc.6 pin:
|
|
79
|
+
`ComponentPropsMap['element:text'].content` and
|
|
80
|
+
`ComponentPropsMap['element:button'].label` are both
|
|
81
|
+
`string | Record< string, string >`, and both refuse a number, a boolean and an
|
|
82
|
+
array. Those three refusals are the controls in the acceptance test, which is
|
|
83
|
+
what keeps a widening distinguishable from a silenced check.
|
|
84
|
+
|
|
85
|
+
Nothing else about the two blocks moves. A plain-string `content` / `label`
|
|
86
|
+
validates exactly as before, values matching neither arm are still reported, and
|
|
87
|
+
no other manifest entry changes shape — the public manifest now carries seven
|
|
88
|
+
array-valued input types, the five from objectui#3832 plus these two, with the
|
|
89
|
+
remaining 57 public blocks serializing byte for byte as they did.
|
|
90
|
+
|
|
91
|
+
`record:alert`'s renderer-local prop type is corrected in the same pass
|
|
92
|
+
(`plugin-detail`): its `title` / `body` were still typed `string` while the same
|
|
93
|
+
file resolves both through `pickLocalized` and the block's published `inputs`
|
|
94
|
+
have declared `['string', 'object']` since objectui#3832, so the two slots were
|
|
95
|
+
narrower than both the renderer and the block's own published surface. The type
|
|
96
|
+
is not exported, so no consumer was misled and no published surface changes. The
|
|
97
|
+
CTA's `action.label` one level down is left alone on purpose (objectui#4998):
|
|
98
|
+
`action` is published as a bare `object` whose member shape lives in prose, so
|
|
99
|
+
there are no declared arms for it to be aligned against yet.
|
|
100
|
+
- a2a9747: `FieldValidationRules.pattern.value` narrows to `RegExp`, and the form renderer reports unrecognized validation rule names loudly (objectui#5099, maintainer ruling 2026-08-18).
|
|
101
|
+
|
|
102
|
+
**BREAKING for hand-written form schemas — deliberately declared `minor`.** This
|
|
103
|
+
repo's version policy reserves `major` for tracking `@objectstack` majors and is
|
|
104
|
+
mechanically enforced (`scripts/check-changeset-no-major.mjs`); per that policy,
|
|
105
|
+
objectui's own breaking changes ship as `minor` with the breaking semantics
|
|
106
|
+
stated plainly here:
|
|
107
|
+
|
|
108
|
+
- **What breaks:** `validation: { pattern: { value: '^…$', message } }` with a
|
|
109
|
+
**string** value no longer compiles. Write a `RegExp` literal instead:
|
|
110
|
+
`pattern: { value: /^…$/, message }`.
|
|
111
|
+
- **Why red is the fix, not the damage:** react-hook-form applies `pattern`
|
|
112
|
+
only when `value instanceof RegExp`, and the renderer's single read point
|
|
113
|
+
spreads `validation` verbatim — so every string pattern accepted by the old
|
|
114
|
+
type ran **zero** validations, silently. Callers turning red were not
|
|
115
|
+
validating anything yesterday; the error converts silent non-validation into
|
|
116
|
+
explicit failure at authoring time.
|
|
117
|
+
- **Unaffected:** the metadata route. `FieldSchema.pattern` (a string in field
|
|
118
|
+
metadata) is still compiled by `buildValidationRules` in `@object-ui/fields`
|
|
119
|
+
via `new RegExp(...)` before it reaches the renderer.
|
|
120
|
+
|
|
121
|
+
Also, per the same ruling's second limb, the form renderer now reports rule
|
|
122
|
+
names react-hook-form does not run (`console.error`, message doubles as the fix
|
|
123
|
+
instruction): a misspelled `minlength`, an invented `email`, or numeric keys
|
|
124
|
+
left by spreading an array into `validation` shout instead of vanishing. The
|
|
125
|
+
recognized set is pinned against the installed react-hook-form bundle so a
|
|
126
|
+
future bump cannot silently rot the diagnostic. The ruling's rejected half is
|
|
127
|
+
equally binding and equally pinned by test: the read point does **not** compile
|
|
128
|
+
string patterns — that consumer-side tolerance would harden the ambiguous
|
|
129
|
+
declaration into contract (AGENTS.md #0.1).
|
|
130
|
+
- 232f61a: Form-field type resolution no longer falls back to `ui`-namespace SDUI node renderers.
|
|
131
|
+
|
|
132
|
+
A `FormSchema` field's `type` now resolves a `field:`-namespaced widget or takes
|
|
133
|
+
the builtin `default` input branch. It no longer falls back to
|
|
134
|
+
`ComponentRegistry.get(type)` — the bare name in whatever namespace happened to
|
|
135
|
+
hold it. (Maintainer ruling of 2026-08-19 on objectui#5254, option B.)
|
|
136
|
+
|
|
137
|
+
**This is a behaviour change, and it is the point of the change, not a side
|
|
138
|
+
effect.** A spelling that resolved yesterday stops resolving: a form field whose
|
|
139
|
+
`type` names a non-field component renders the default input instead of that
|
|
140
|
+
component. Measured on the built-in (no-`registerAllFields()`) path, the removed
|
|
141
|
+
fallback answered **126** bare names — `div`, `h1`, `card`, `button`, `form`,
|
|
142
|
+
`alert`, `badge`, the display `text` widget — and 116 of them with the fields
|
|
143
|
+
package registered as well. Marked `minor` for that reason. It is released as a
|
|
144
|
+
behaviour change rather than a fix because callers cannot tell from their own
|
|
145
|
+
metadata which of the two rules answered them; anyone who deliberately pointed a
|
|
146
|
+
form field at an SDUI component was relying on a rule no contract stated, and
|
|
147
|
+
that reliance now needs a `field:`-namespaced widget instead.
|
|
148
|
+
|
|
149
|
+
Nothing changes for the two paths that carry real traffic. With
|
|
150
|
+
`registerAllFields()` — the production configuration — every affected field type
|
|
151
|
+
already resolved its own `field:` widget (`email` to `field:email`, `password`
|
|
152
|
+
to `field:password`, `text` to `field:text`), and object-derived forms go
|
|
153
|
+
through `mapFieldTypeToFormType`, which has always emitted the `field:`-prefixed
|
|
154
|
+
id. Rendering one of these components as a top-level SDUI **node** is untouched:
|
|
155
|
+
this rule governs field resolution only.
|
|
156
|
+
|
|
157
|
+
What the fallback was producing on the built-in path, for
|
|
158
|
+
`{ name: 'contact', type: 'email', max_length: 50 }`:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
attrs=["class","id","max_length","field","aria-describedby",
|
|
162
|
+
"aria-invalid","type","value","name"] maxlength=null
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`email` and `password` are registered as `ui`-namespace node renderers for
|
|
166
|
+
top-level `{ type: 'email' }` nodes, so reached as a *field* they received the
|
|
167
|
+
field-widget prop bundle they do not implement and spread it onto the element:
|
|
168
|
+
the raw metadata object landed as `field="[object Object]"`, `max_length`
|
|
169
|
+
landed as an inert attribute with no cap in effect (`maxlength` null), and the
|
|
170
|
+
node renderer's own `<Label>` gave the control a second `<label>` on top of the
|
|
171
|
+
form's. All three are gone; the declared ceiling now actually caps
|
|
172
|
+
(`maxlength="50"`).
|
|
173
|
+
|
|
174
|
+
`email` and `password` keep rendering the native input they always rendered.
|
|
175
|
+
The default branch derives `<input type>` from those two declared field types
|
|
176
|
+
(`EmailFieldMetadata` / `PasswordFieldMetadata`), because `inputType` there is
|
|
177
|
+
whatever the author wrote and a plain `{ name, type: 'password' }` authors none
|
|
178
|
+
— without it that field would have rendered `type="text"` and shown a secret in
|
|
179
|
+
clear text. An explicitly authored `inputType` still wins. Other declared types
|
|
180
|
+
with a native HTML equivalent (`url`, `phone`, `number`, `color`, `date`)
|
|
181
|
+
already took this branch as `type="text"` and are unchanged.
|
|
182
|
+
- 5673576: fix(components): call `FormSchema.onChange` — the declared callback the form renderer never invoked (objectui#4259)
|
|
183
|
+
|
|
184
|
+
`FormSchema` declares four lifecycle callbacks and the form renderer
|
|
185
|
+
destructures all four off `schema` in one block: `onSubmit`, `onChange`,
|
|
186
|
+
`onDirtyChange`, `onCancel`. Three were wired. `onChange` was destructured and
|
|
187
|
+
then never referenced again — the destructure was its only occurrence in the
|
|
188
|
+
whole file — so a consumer who authored it got a typed, exported, documented,
|
|
189
|
+
autocompleted callback that did nothing at all. No warning, no dev-mode notice,
|
|
190
|
+
no type error: the declaration said it was supported.
|
|
191
|
+
|
|
192
|
+
It is now called with the live form values whenever a value changes, through the
|
|
193
|
+
same `form.watch` subscription plumbing the `onDirtyChange` and `onAction`
|
|
194
|
+
channels already use, so the value channel cannot drift into its own schedule.
|
|
195
|
+
|
|
196
|
+
Two properties are deliberate and pinned by tests:
|
|
197
|
+
|
|
198
|
+
- **The subscription is guarded.** A schema that authors no `onChange`
|
|
199
|
+
establishes no subscription at all, exactly like the existing `onAction`
|
|
200
|
+
channel and unlike the unconditional `onDirtyChange` one. Watching
|
|
201
|
+
unconditionally would have put a third `form.watch` on every form in the
|
|
202
|
+
product on behalf of callers who asked for nothing; honouring the declaration
|
|
203
|
+
is meant to be purely additive for everyone else.
|
|
204
|
+
- **It runs in the layout phase**, matching the `defaultValues` reset and the
|
|
205
|
+
two subscriptions beside it. React runs every layout destroy before any layout
|
|
206
|
+
create, so a caller passing a fresh inline arrow each render — the common
|
|
207
|
+
shape — has the subscription torn down before the reset and re-established
|
|
208
|
+
after. That is what keeps a record landing in edit mode from being reported to
|
|
209
|
+
the host as if the user had edited every field it filled. A passive effect
|
|
210
|
+
inverts that order.
|
|
211
|
+
|
|
212
|
+
The callback receives the form values, per its declared
|
|
213
|
+
`(data) => void` signature — not a DOM event. A top-level `onChange` spread onto
|
|
214
|
+
the form node is still stripped before it can reach the `form` element, where it
|
|
215
|
+
would have fired with a SyntheticEvent instead; that block's behaviour is
|
|
216
|
+
unchanged, only its now-outdated comment was corrected.
|
|
217
|
+
|
|
218
|
+
No change to `@object-ui/types` — the declaration was already there and already
|
|
219
|
+
correct. This is the renderer starting to honour it.
|
|
220
|
+
- 911ceaa: The fullscreen long-text dialog announces the field's validation state and carries the field's name
|
|
221
|
+
|
|
222
|
+
objectui#4824, objectui#4832.
|
|
223
|
+
|
|
224
|
+
`mobile.fullscreenLongText` is a shipped opt-in, and with it on the phone user
|
|
225
|
+
edits long text in this dialog and nowhere else. Measured on all three surfaces
|
|
226
|
+
that render the dialog — `TextAreaField`, `RichTextField`, and the form
|
|
227
|
+
renderer's built-in `textarea` branch — with the field genuinely invalid at that
|
|
228
|
+
moment:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
INLINE richtext aria-invalid= true
|
|
232
|
+
DIALOG richtext aria-invalid= false aria-describedby= null
|
|
233
|
+
INLINE textarea aria-invalid= true
|
|
234
|
+
DIALOG textarea aria-invalid= null aria-describedby= null
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
and the accessible name of every dialog control empty, against `F` on every
|
|
238
|
+
inline one. The rich-text row is the sharp half: the dialog was not silent about
|
|
239
|
+
the failure, it was announcing the OPPOSITE of the inline control for the same
|
|
240
|
+
field at the same moment, because `RichTextEditorSurface` computed
|
|
241
|
+
`aria-invalid={!!error}` from an `error` prop the dialog rendering never
|
|
242
|
+
received. 3 surfaces, 3 broken, one cause: the dialog's control is built from
|
|
243
|
+
scratch by the host, so none of the wiring the inline control gets from the form
|
|
244
|
+
renderer reaches it.
|
|
245
|
+
|
|
246
|
+
**Answered once, in the primitive.** `FullscreenEditor` now takes the field's
|
|
247
|
+
`error` and owns what the dialog does with it: it renders the message in a
|
|
248
|
+
dialog-local node, and hands `children` a required fourth argument — a
|
|
249
|
+
spreadable set of DOM attributes — carrying `aria-labelledby` (the dialog
|
|
250
|
+
title's text, i.e. the field label #3393 already put there), `aria-invalid`, and
|
|
251
|
+
`aria-errormessage` naming that node. The host spreads it; the host never learns
|
|
252
|
+
an id, so it cannot name the wrong node, cannot compose the attributes subtly
|
|
253
|
+
wrong, and cannot compute its own `aria-invalid` from a prop it forgot to plumb.
|
|
254
|
+
Three hosts hand-answering this is the shape that produced three identical
|
|
255
|
+
holes.
|
|
256
|
+
|
|
257
|
+
**On objectui#3222's "the text belongs to `FormMessage`".** The maintainer's
|
|
258
|
+
ruling of 2026-08-16 restates that rule as what it was always protecting —
|
|
259
|
+
only one copy of the error text is in the accessibility tree at any moment —
|
|
260
|
+
which the dialog-local node satisfies: it exists only while the dialog is open,
|
|
261
|
+
and for exactly that window Radix `aria-hidden`s everything outside the modal,
|
|
262
|
+
`FormMessage` included. The shortcut of pointing the dialog control's
|
|
263
|
+
`aria-describedby` / `aria-errormessage` at the host's `FormMessage` id is
|
|
264
|
+
forbidden rather than merely unused: it resolves to a node that is `aria-hidden`
|
|
265
|
+
for the whole time the reference is live (an ARIA MUST violation), and neither
|
|
266
|
+
happy-dom nor jsdom can see the difference — which is why every new pin asserts
|
|
267
|
+
that the named node is inside THIS dialog, not merely that it exists.
|
|
268
|
+
|
|
269
|
+
`aria-errormessage` carries a single IDREF and is emitted only alongside
|
|
270
|
+
`aria-invalid="true"`. It is deliberately not folded into the host's
|
|
271
|
+
`aria-describedby` chain, which on the textarea surface already carries the
|
|
272
|
+
fullscreen character counter's sentence.
|
|
273
|
+
|
|
274
|
+
**The name reuses the visible title rather than minting a second author for it**
|
|
275
|
+
(#3978): `aria-labelledby` points at a span inside `DialogTitle`, not at
|
|
276
|
+
`DialogTitle` itself — Radix renders the title as `h2` with the id its own
|
|
277
|
+
`DialogContent` `aria-labelledby` names, so putting an id on it would buy the
|
|
278
|
+
control a name at the cost of the dialog's.
|
|
279
|
+
|
|
280
|
+
**Breaking (shipped as `minor`, see below), `@object-ui/components` only.**
|
|
281
|
+
`FullscreenEditorProps.error` is REQUIRED, not optional, and
|
|
282
|
+
`FullscreenEditorProps['children']` takes a fourth argument.
|
|
283
|
+
|
|
284
|
+
FROM → TO for an out-of-repo host:
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
<FullscreenEditor value={v} onCommit={c} label={l} testIdPrefix="x">
|
|
288
|
+
{(draft, setDraft, disabled) => <textarea … />}
|
|
289
|
+
</FullscreenEditor>
|
|
290
|
+
|
|
291
|
+
<FullscreenEditor value={v} onCommit={c} label={l} testIdPrefix="x" error={err}>
|
|
292
|
+
{(draft, setDraft, disabled, aria) => <textarea {...aria} … />}
|
|
293
|
+
</FullscreenEditor>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
A render prop may still declare fewer parameters, so only `error` fails to
|
|
297
|
+
compile — which is the point of making it required. Every consumer already has
|
|
298
|
+
the value at hand (registered widgets take `error` off the widget props
|
|
299
|
+
contract, objectui#3222; the built-in branch reads `fieldState.error?.message`),
|
|
300
|
+
and an omitted `error` reproduces this defect exactly: a dialog announcing
|
|
301
|
+
`aria-invalid="false"` for a field its own form has already failed. An optional
|
|
302
|
+
key was forgotten by three surfaces in a row; a required one cannot be.
|
|
303
|
+
|
|
304
|
+
`@object-ui/fields` is `patch`: `FullscreenFieldEditor` is internal to that
|
|
305
|
+
package (not re-exported from its entry), so nothing in its public surface
|
|
306
|
+
changes — only the behaviour of the two long-text widgets' dialogs.
|
|
307
|
+
|
|
308
|
+
`minor` rather than `major` follows the repo's standing retirement precedent
|
|
309
|
+
(AGENTS.md §版本号策略, enforced by `scripts/check-changeset-no-major.mjs`): all
|
|
310
|
+
publishable packages sit in one `fixed` group, so a `major` here would carry the
|
|
311
|
+
whole family up against an `@objectstack` that has not moved.
|
|
312
|
+
- 98eab36: Publish the five `@objectstack/spec` 17.0.0 keys the renderers already honoured, so
|
|
313
|
+
authors can discover them
|
|
314
|
+
|
|
315
|
+
`page:header.maxVisible`, `page:header.mobileMaxVisible`, `page:tabs.alwaysShowStrip`,
|
|
316
|
+
`record:details.inlineEdit` and `record:details.showHeader` are declared by the spec and
|
|
317
|
+
read by the renderers today, and none of them was in its block's published `inputs`. That
|
|
318
|
+
is the direction nothing reports: `gen-manifest.ts` left all five out of
|
|
319
|
+
`sdui.manifest.json` and `sdui-intrinsics.d.ts`, so they were in no designer panel and no
|
|
320
|
+
generated type; `sdui-parser`'s prop walk reported `unknown-prop` on an author who wrote
|
|
321
|
+
one anyway; and the renderer honoured it regardless. Measured on the console's own
|
|
322
|
+
manifest before this change, all five drew
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
unknown-prop: page:header has no prop "maxVisible"
|
|
326
|
+
unknown-prop: page:header has no prop "mobileMaxVisible"
|
|
327
|
+
unknown-prop: page:tabs has no prop "alwaysShowStrip"
|
|
328
|
+
unknown-prop: record:details has no prop "inlineEdit"
|
|
329
|
+
unknown-prop: record:details has no prop "showHeader"
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
and now draw nothing. Same defect as `record:details.hideFields` in objectui#3808 and
|
|
333
|
+
`readonly` in objectui#3407; it could not land until the GA pin moved (objectui#4636),
|
|
334
|
+
because the pre-GA pin declared none of the five and publishing them would have failed the
|
|
335
|
+
repo-wide parity gate's forward direction.
|
|
336
|
+
|
|
337
|
+
Each entry carries a description, because for these keys the discoverability IS the fix.
|
|
338
|
+
Two are worth reading before use:
|
|
339
|
+
|
|
340
|
+
- `maxVisible` / `mobileMaxVisible` are positive integers — the contract rejects `0` and
|
|
341
|
+
fractional values — and they do not govern every action: an action declaring
|
|
342
|
+
`record_more` without `record_header`, and any action with `component: 'action:menu'`,
|
|
343
|
+
is routed to the overflow menu regardless of the budget.
|
|
344
|
+
- `inlineEdit` is an opt-OUT only. The value is combined with the object's own resolved
|
|
345
|
+
editability (ADR-0103) and with the server's effective API operation set, so `false`
|
|
346
|
+
always wins while `true` cannot open editing the platform refuses.
|
|
347
|
+
|
|
348
|
+
**`page:tabs` also gains a read.** `alwaysShowStrip` was honoured only as
|
|
349
|
+
`schema.properties.alwaysShowStrip`, while `inputs` publishes TOP-LEVEL keys — the shape
|
|
350
|
+
the manifest whitelists, the generated types declare and the JSX-page compiler validates.
|
|
351
|
+
Measured on a one-tab schema: the wrapped form showed the strip, the flat form did not, so
|
|
352
|
+
publishing the key alone would have advertised a write the renderer throws away. The
|
|
353
|
+
canonical top-level arm is read first now, with the `properties` arm kept for paths that
|
|
354
|
+
reach the renderer without `SchemaRenderer`'s hoist — the same dual read `maxVisible` has
|
|
355
|
+
always had. This can only ever ADD a strip to a one-tab page; multi-tab pages are
|
|
356
|
+
unaffected, and `false` and non-boolean values both read as "not set".
|
|
357
|
+
|
|
358
|
+
The five GA-pending entries that held this card's place in
|
|
359
|
+
`registry-inputs-spec-parity.test.ts` are deleted, which is what the gate's own
|
|
360
|
+
`carries no stale unpublished-key exemption` check demands once the keys are published.
|
|
361
|
+
- 167ec42: `ComponentMeta.labelling` grows a third value: `'control' | 'group' | 'display'`
|
|
362
|
+
(objectui#4857, ruled jointly with objectui#4871 as the single repo-wide vocabulary for
|
|
363
|
+
"how does a host learn what a widget will render"). `'display'` declares a widget whose
|
|
364
|
+
whole surface is a pure display in EVERY state — no focusable control, nothing a
|
|
365
|
+
`<label for>` could ever reach.
|
|
366
|
+
|
|
367
|
+
The form renderer answers the declaration with the objectui#4788 host container (field
|
|
368
|
+
id + `aria-labelledby` + `aria-describedby` + `role="group"`) in the editable state too;
|
|
369
|
+
the `readonly === true` arm keeps its exact #4788 semantics for undeclared widgets. The
|
|
370
|
+
display-only four (`formula` / `summary` / `auto_number` / `vector`) declare `'display'`
|
|
371
|
+
— on the real object-form path they arrive `disabled`, never `readonly` (a deliberate
|
|
372
|
+
distinction this change does not touch), so their visible labels pointed `for` at an id
|
|
373
|
+
no element carried and their help text had zero consumers in every editable form.
|
|
374
|
+
|
|
375
|
+
`grid` was re-measured before being classified: its only bare-config focusable is the
|
|
376
|
+
auxiliary "Add line" button (routing `for` there would have label clicks insert rows),
|
|
377
|
+
and every realistic config is a table of per-cell inputs — a composite. It declares
|
|
378
|
+
`labelling: 'group'` and its root container now consumes the host id, name and
|
|
379
|
+
description, exactly like `address` / `checkboxes`.
|
|
380
|
+
|
|
381
|
+
Companion registry gate: `FIELD_WIDGET_LABELLING` (exported) is a `Record` keyed by the
|
|
382
|
+
field-widget map's own literal key union, so registering a widget without deciding its
|
|
383
|
+
labelling is a compile error rather than a silent fall-through to the dangling-`for`
|
|
384
|
+
path, and the declaration test asserts the registered meta agrees with it key by key.
|
|
385
|
+
- f1d4748: Remove the retired `striped` / `bordered` / `virtualScroll` list-view surface
|
|
386
|
+
|
|
387
|
+
objectstack#7176 retired `list.striped`, `list.bordered` and `list.virtualScroll`
|
|
388
|
+
from the spec after measuring every objectui reader as pass-through: each one
|
|
389
|
+
copied the key onward and no renderer ever applied it. objectui stops declaring,
|
|
390
|
+
typing and forwarding them.
|
|
391
|
+
|
|
392
|
+
Off the chain: the `@objectstack/spec` list-view bridge in `@object-ui/react`,
|
|
393
|
+
`ListView`'s child-view props in `@object-ui/plugin-list`, both `ObjectView`
|
|
394
|
+
relays (`@object-ui/plugin-view` and `@object-ui/app-shell`), the `ObjectGridSchema`
|
|
395
|
+
and `NamedListView` declarations in `@object-ui/types` (interface and zod),
|
|
396
|
+
`ObjectGrid.component.yml` in `@object-ui/components`, and the page-block
|
|
397
|
+
inspector's `striped` / `bordered` toggles in the metadata-admin designer.
|
|
398
|
+
|
|
399
|
+
Behaviour is unchanged: nothing read these keys, so nothing rendered differently
|
|
400
|
+
for them. Stored view metadata that still carries one keeps validating — the keys
|
|
401
|
+
are simply no longer relayed. `ListViewSchema` continues to take the spec's
|
|
402
|
+
list-view fields by reference, so the protocol's own retirement tombstones
|
|
403
|
+
arrive with the next `@objectstack/spec` bump and reject the keys at the
|
|
404
|
+
authoring boundary. Restoring any of the three as live surface requires an
|
|
405
|
+
implementation card filed first, per the ruling.
|
|
406
|
+
- 58b8346: Settle the two declared-but-unread keys on `AccordionItem`: retire `icon`, wire
|
|
407
|
+
`disabled` (objectui#4652).
|
|
408
|
+
|
|
409
|
+
The same defect as objectui#4632 (PR #4651), one interface up in the same file.
|
|
410
|
+
`AccordionItem` declared `disabled?: boolean` and `icon?: string` while the
|
|
411
|
+
`accordion` renderer read neither — it mapped items to `value`/`title`/`content`
|
|
412
|
+
and dropped the rest. Nothing went red: an author who declared either key got a
|
|
413
|
+
correctly rendered accordion with the key silently ignored.
|
|
414
|
+
|
|
415
|
+
The two keys are settled in opposite directions, by measurement rather than by
|
|
416
|
+
symmetry. A full corpus sweep (schema catalog, docs, example apps, and this
|
|
417
|
+
repo's `objectstack` sibling checkout) found **zero** sites authoring either key
|
|
418
|
+
on an `AccordionItem`:
|
|
419
|
+
|
|
420
|
+
- **`icon` is retired** from the TypeScript interface and from the
|
|
421
|
+
`AccordionItemSchema` Zod mirror. It had zero measured pull anywhere in the
|
|
422
|
+
corpus and no established convention to lean on, so under this platform's
|
|
423
|
+
declared=enforced doctrine it is removed rather than speculatively
|
|
424
|
+
implemented.
|
|
425
|
+
- **`disabled` is honored**, despite also having zero catalog pull today.
|
|
426
|
+
Item-level `disabled` is already established live convention in this
|
|
427
|
+
codebase — `tabs`, `select`, `dropdown-menu`, `menubar`, `context-menu` and
|
|
428
|
+
(objectui#4632) `toggle-group` all forward it, and `accordion` was the next
|
|
429
|
+
outlier. The underlying Radix accordion item supports `disabled` natively, so
|
|
430
|
+
the renderer forwarding one prop is the whole change; the synced
|
|
431
|
+
`ui/accordion.tsx` primitive is untouched. The schema catalog's
|
|
432
|
+
`basic-accordion` example now demonstrates a disabled item.
|
|
433
|
+
|
|
434
|
+
**Breaking for TypeScript authors of `icon` only** (marked `minor` per this
|
|
435
|
+
repo's version-alignment rule, which reserves `major` for following
|
|
436
|
+
`@objectstack` across a major — see AGENTS.md's 版本号策略 and the identical
|
|
437
|
+
classification PR #4651 used for `ToggleGroupItem.icon`). Runtime behaviour of
|
|
438
|
+
an authored `icon` is unchanged — it rendered nothing before and renders
|
|
439
|
+
nothing now; what changes is that the contract no longer claims otherwise, so
|
|
440
|
+
the mistake surfaces at authoring time. Authored `disabled` changes from
|
|
441
|
+
silently ignored to actually disabling that one item (and blocking its
|
|
442
|
+
expand/collapse).
|
|
443
|
+
- 93fe362: layout: `flex` and `container` now honour a declared scale value of `0`
|
|
444
|
+
|
|
445
|
+
`FlexSchema.gap` and `ContainerSchema.padding` are declared `number`, and both
|
|
446
|
+
renderers already carried an explicit zero branch (`gap === 0 && 'gap-0'`,
|
|
447
|
+
`padding === 0 && 'p-0'`). Neither branch was reachable: the value was read with
|
|
448
|
+
`||`, so a declared `0` was folded into the default before the branch was tested.
|
|
449
|
+
A `flex` asking for no gap rendered `gap-1.5 sm:gap-2`, and a `container` asking
|
|
450
|
+
for no padding rendered `p-2 sm:p-3 md:p-4` — the JSON said one thing and the DOM
|
|
451
|
+
did another, with nothing reported.
|
|
452
|
+
|
|
453
|
+
Both now read the value with `??`, matching how the sibling `stack` and `grid`
|
|
454
|
+
renderers already read theirs. Omitting the key still applies the same defaults
|
|
455
|
+
(`gap: 2`, `padding: 4`); only an explicitly declared `0` changes, and no node in
|
|
456
|
+
this repository declared either key as `0` before this change.
|
|
457
|
+
- 99bd015: Settle the two declared-but-unread keys on `ToggleGroupItem`: retire `icon`, wire
|
|
458
|
+
`disabled` (objectui#4632).
|
|
459
|
+
|
|
460
|
+
`ToggleGroupItem` declared `icon?: string` and `disabled?: boolean` while the
|
|
461
|
+
`toggle-group` renderer read neither — it mapped items to value + aria-label +
|
|
462
|
+
label and dropped the rest. Nothing went red, which is what made it durable: an
|
|
463
|
+
author who declared either key got a correctly rendered group with the key
|
|
464
|
+
silently ignored, and the schema catalog (the corpus AI authoring tools retrieve
|
|
465
|
+
from) was teaching `icon` on all three items of
|
|
466
|
+
`components-disclosure-toggle-group/with-labels`.
|
|
467
|
+
|
|
468
|
+
The two keys are settled in opposite directions, by measurement rather than by
|
|
469
|
+
symmetry:
|
|
470
|
+
|
|
471
|
+
- **`icon` is retired** from the TypeScript interface, from the `ToggleGroupItemSchema`
|
|
472
|
+
Zod mirror, from that catalog entry and from the component's docs page. It had zero
|
|
473
|
+
measured pull — across the repo the single catalog entry was the only site authoring
|
|
474
|
+
it, no application code or example app declared it, and no renderer resolved it.
|
|
475
|
+
- **`disabled` is honored.** Item-level `disabled` is already live convention here —
|
|
476
|
+
`tabs`, `select`, `dropdown-menu`, `menubar` and `context-menu` all forward it, and
|
|
477
|
+
`toggle-group` was the lone outlier. The underlying Radix item supports it natively,
|
|
478
|
+
so the renderer forwarding the prop is the whole change; the synced `ui/toggle-group.tsx`
|
|
479
|
+
primitive is untouched.
|
|
480
|
+
|
|
481
|
+
**Breaking for TypeScript authors of `icon` only** (marked `minor` per this repo's
|
|
482
|
+
version-alignment rule, which reserves `major` for following `@objectstack` across a
|
|
483
|
+
major). Runtime behaviour of an authored `icon` is unchanged — it rendered nothing
|
|
484
|
+
before and renders nothing now; what changes is that the contract no longer claims
|
|
485
|
+
otherwise, so the mistake surfaces at authoring time. Authored `disabled` changes from
|
|
486
|
+
silently ignored to actually disabling that one item.
|
|
487
|
+
|
|
488
|
+
### Patch Changes
|
|
489
|
+
|
|
490
|
+
- 460c4d0: The built-in form `input` branch now honours a declared ceiling in both authored spellings.
|
|
491
|
+
|
|
492
|
+
The branch spread its leftover field props straight onto the element and never
|
|
493
|
+
read the declared ceiling, so one declaration produced two different outcomes.
|
|
494
|
+
Measured on `origin/main`, rendering the built-in branch (no `registerAllFields()`)
|
|
495
|
+
and dumping the element's `getAttributeNames()` / `getAttribute('maxlength')`:
|
|
496
|
+
|
|
497
|
+
| declaration | `maxlength` on the element | effect |
|
|
498
|
+
|---|---|---|
|
|
499
|
+
| `maxLength: 50` | `"50"` | capped — but only by the coincidence that `maxLength` names a real DOM attribute |
|
|
500
|
+
| `max_length: 50` | `null`, plus a stray `max_length="50"` | no cap at all, and invalid HTML |
|
|
501
|
+
|
|
502
|
+
Two distinct defects: the missing cap, and an inert attribute on the DOM that
|
|
503
|
+
reads like a working cap to whoever greps the file next.
|
|
504
|
+
|
|
505
|
+
`max_length` is a live authoring spelling, not a fossil. The registered
|
|
506
|
+
`field:*` widgets have dual-read `maxLength ?? max_length` since framework#1878
|
|
507
|
+
§3, all three producers of a form field normalize it (`ObjectForm`,
|
|
508
|
+
`sectionFields`, `EmbeddableForm.applyDefaultMaxLengths`) and `@object-ui/types`
|
|
509
|
+
declares it on several field types. Every reader in the repo honoured it except
|
|
510
|
+
this branch — which is precisely the one serving a hand-written `FormSchema` fed
|
|
511
|
+
straight to the renderer, where no producer sits in between to normalize it and
|
|
512
|
+
the author is the producer. This is the same mechanism objectui#3439 resolved
|
|
513
|
+
for the built-in `textarea` branch.
|
|
514
|
+
|
|
515
|
+
The legacy key is destructured off locally rather than added to the shared
|
|
516
|
+
`stripRendererOnlyProps` list: that helper feeds every branch
|
|
517
|
+
(`checkbox`/`switch`/`select`/`default` all share `domFieldProps`), so extending
|
|
518
|
+
it would change what reaches the DOM for widgets this change neither fixes nor
|
|
519
|
+
tests. The neighbouring `textarea` branch strips it the same local way.
|
|
520
|
+
|
|
521
|
+
Scope, stated because the sibling card resolved more than this one: the ceiling
|
|
522
|
+
only. Whether a single-line input should also carry the visible `{n}/{max}`
|
|
523
|
+
counter and the announced limit that the `textarea` branch grew in
|
|
524
|
+
objectui#3439 is an independent design trade-off that does not follow from that
|
|
525
|
+
card's conclusion, and is deliberately left undecided here.
|
|
526
|
+
- 0ae27f7: The form renderer's built-in `textarea` branch now honours a declared character cap the same way the registered `field:textarea` widget does.
|
|
527
|
+
|
|
528
|
+
One `maxLength` declaration produced two experiences. The registered path has
|
|
529
|
+
shipped four things since objectui#3406/#3408/#3417 — the native cap, visible
|
|
530
|
+
`{n}/{max}` digits, a description reached through `aria-describedby` so the
|
|
531
|
+
limit is announced on focus, and a threshold-gated debounced near-limit notice.
|
|
532
|
+
The built-in branch — the path standalone and embedded hosts take, the ones that
|
|
533
|
+
call no `registerAllFields()` — shipped a subset of one of them.
|
|
534
|
+
|
|
535
|
+
The accessibility half is the half that mattered: a screen-reader user on this
|
|
536
|
+
path learned the field's limit only as a validation error AFTER submitting. All
|
|
537
|
+
four affordances now render on both of the branch's surfaces (the inline control
|
|
538
|
+
and the fullscreen dialog), from the SAME `CharacterCount` component the widget
|
|
539
|
+
renders rather than a second copy of it.
|
|
540
|
+
|
|
541
|
+
Also fixed, and wider than the visible gap: the branch never READ the cap, it
|
|
542
|
+
only spread its leftover field props onto the element. A camelCase `maxLength`
|
|
543
|
+
therefore worked by coincidence — it names a real DOM attribute — while the
|
|
544
|
+
legacy `max_length` spelling, which the registered widget and all three
|
|
545
|
+
producers of a form field have dual-read since framework#1878 §3, landed as a
|
|
546
|
+
stray inert `max_length="…"` attribute and capped nothing at all. The branch now
|
|
547
|
+
resolves both spellings and keeps the non-attribute spelling off the DOM.
|
|
548
|
+
|
|
549
|
+
`CharacterCount` moved from `@object-ui/fields` to `@object-ui/components`, the
|
|
550
|
+
package both render paths may import, in the direction and for the reason
|
|
551
|
+
objectui#3398 measured for `FullscreenEditor`. It was internal to `fields` (never
|
|
552
|
+
exported from that package's barrel), so no published export changed; it is a
|
|
553
|
+
new export of `@object-ui/components`. Its copy moved with it onto the same
|
|
554
|
+
`fields.textarea.*` keys with byte-identical English defaults, so the ten locale
|
|
555
|
+
packs need no edit and provider-less rendering is unchanged.
|
|
556
|
+
- 78c0f9a: The form's cascade clear now recognises object-form fields, so a narrowed option list no longer submits a stale value.
|
|
557
|
+
|
|
558
|
+
`field:select` and `select` name the SAME field kind: the object-form path
|
|
559
|
+
(`mapFieldTypeToFormType`) emits the prefixed widget id, hand-written SDUI
|
|
560
|
+
schemas the bare one. The form host's cascade-clear effect (objectui#2284)
|
|
561
|
+
compared the RAW type string against the bare-name set, so every option field
|
|
562
|
+
coming from an OBJECT schema fell out of the effect entirely. Its controlling
|
|
563
|
+
field could change, its option list narrow, and the no-longer-offered value was
|
|
564
|
+
never dropped — the form submitted exactly the stale "china + california" pair
|
|
565
|
+
the effect exists to prevent. Only genuinely cascading fields were affected
|
|
566
|
+
(those carrying a `dependsOn` or a per-option `visibleWhen`); a plain picklist
|
|
567
|
+
has nothing to recompute either way.
|
|
568
|
+
|
|
569
|
+
The comparison now normalizes the type before the lookup, which is what the
|
|
570
|
+
render path a few hundred lines below has done for `isOptionField` since
|
|
571
|
+
objectui#3231 — the two readers of "is this an option field?" no longer
|
|
572
|
+
disagree about what a `select` is. This half was the one missed then.
|
|
573
|
+
|
|
574
|
+
Stated because it is a behavior change and not an equivalent refactor: the
|
|
575
|
+
object-form path gains cascade clearing for the FIRST time. A form whose stored
|
|
576
|
+
value is genuinely excluded by its chosen parent will now clear that value where
|
|
577
|
+
it previously kept it. The narrowing is bounded by the rules already in place
|
|
578
|
+
for the bare-name path, both of which the object path now inherits unchanged: a
|
|
579
|
+
GATED list (a declared `dependsOn` parent still empty) is treated as unknown
|
|
580
|
+
rather than invalid and never deletes anything (objectui#4247), and a field with
|
|
581
|
+
no `dependsOn` and no per-option predicate is never recomputed at all.
|
|
582
|
+
- bbe8b86: The allow-list of option widgets that are fed the live record is now one exported constant, `CASCADE_OPTION_WIDGET_TYPES`, instead of three private copies.
|
|
583
|
+
|
|
584
|
+
`select` / `multiselect` / `radio` / `checkboxes` are the widgets whose OFFERED
|
|
585
|
+
option set is re-resolved against a record (per-option `visibleWhen`, plus the
|
|
586
|
+
`dependsOn` gate), so they are the widgets a surface must thread its live record
|
|
587
|
+
to. Three surfaces feed that one evaluator — the object form, the single-record
|
|
588
|
+
action dialog and the bulk action dialog — and until now each carried its own
|
|
589
|
+
private `new Set([...])` of the same four keys, with a comment in each asking the
|
|
590
|
+
next person to change all three together. Nothing could have reported them
|
|
591
|
+
drifting: every copy passed its own behavioural tests, and a divergence would
|
|
592
|
+
have shown up only as one surface silently disagreeing with another about what
|
|
593
|
+
"the record" is.
|
|
594
|
+
|
|
595
|
+
The set now lives in `@object-ui/core`, next to `resolveCascadingOptions` — the
|
|
596
|
+
evaluator that reads that record — because core is the one package all three
|
|
597
|
+
surfaces already depend on, and it is re-exported from `@object-ui/fields` next
|
|
598
|
+
to `resolveFormWidgetType`, whose output is the vocabulary the keys are written
|
|
599
|
+
in. Both are the same object, pinned by test; each consumer keeps its own
|
|
600
|
+
normalization (`normalizeFieldType` in the form, `resolveFormWidgetType` in the
|
|
601
|
+
dialogs), which agree on these four members.
|
|
602
|
+
|
|
603
|
+
No behaviour changes: the members are identical on all three surfaces, and the
|
|
604
|
+
existing pins for each surface still assert the same records reaching the same
|
|
605
|
+
widgets. The rationale that was repeated in the three copies — including the
|
|
606
|
+
note that the widget-hint picker family (`filter-condition`, `recipient-picker`,
|
|
607
|
+
the lookup family) reads a different sibling key off the same channel and is
|
|
608
|
+
deliberately NOT in this set — is now stated once, in the constant's own
|
|
609
|
+
documentation. Whether the action and bulk dialogs should ever feed those
|
|
610
|
+
pickers stays an open question (objectui#4771), unchanged by this convergence.
|
|
611
|
+
- 2e82ab2: The config panel footer translates: `ConfigPanelRenderer`'s Save / Discard labels come from the locale pack.
|
|
612
|
+
|
|
613
|
+
`saveLabel` and `discardLabel` carried the English literals `'Save'` and
|
|
614
|
+
`'Discard'` as parameter defaults, and no caller in the repo passes either prop,
|
|
615
|
+
so the sticky footer that appears the moment a config draft is dirty stayed
|
|
616
|
+
English in every locale — inside panels whose every other string had already
|
|
617
|
+
been routed through `t()`. The fix is in the renderer rather than per-caller:
|
|
618
|
+
the footer is the renderer's own chrome, so a caller-side fix would translate
|
|
619
|
+
one panel's footer and leave the next host's English.
|
|
620
|
+
|
|
621
|
+
Both labels now resolve through `createSafeTranslation` — the mechanism this
|
|
622
|
+
package already uses for its built-in copy in `form.tsx`,
|
|
623
|
+
`fullscreen-editor.tsx`, `data-table.tsx` and friends. An explicitly passed
|
|
624
|
+
`saveLabel` / `discardLabel` still wins, unchanged and untranslated.
|
|
625
|
+
|
|
626
|
+
`common.save` is reused rather than twinned: it already ships `Save` in all ten
|
|
627
|
+
packs and is what the console's other save buttons read. `common.discard` is
|
|
628
|
+
new, because the packs carried no shared spelling of the word — the three that
|
|
629
|
+
existed are each scoped to one surface (`form.discard`,
|
|
630
|
+
`console.settingsView.discard`, `console.objectView.discard`) and the last of
|
|
631
|
+
them diverges from the other two in zh/ko/fr. Its ten values are the majority
|
|
632
|
+
spelling, byte-identical to `form.discard` and `console.settingsView.discard`.
|
|
633
|
+
|
|
634
|
+
Both English defaults are byte-identical to the literals they replace, so a
|
|
635
|
+
host that mounts no `I18nProvider` renders exactly what it did before.
|
|
636
|
+
- 40d3a33: `div` 的废弃提示按 provenance 收窄:只对 **JSON 作者面**的节点报,不再对 `kind:'html'` tier 自己解析出的节点开火。
|
|
637
|
+
|
|
638
|
+
html tier 的页面是一段受限 JSX/Tailwind 文本,由引擎自己的解析器编译(只解析、不执行),标签名原样映射成节点 —— 作者在那一层写下的盒子标签,是该 tier 词表里的一等成员,**没有别的拼法可迁移**。提示照旧对他们开火,给的还是 JSON 作者面的替代建议:一条谁都无法执行的提示不是废弃,是噪声;它同时意味着这个类型永远退不掉,因为引擎自己的编译器一直在产出它。
|
|
639
|
+
|
|
640
|
+
判据是**来源**,由生产者确立:解析器给它产出的每个节点打一个 symbol 标记(`Symbol.for` 注册键),渲染器读这个标记。symbol 对 `JSON.stringify` / `Object.keys` / DOM 全部不可见 —— 所以它既不会落进被持久化的文档,也就无法被一份(手写或 AI 生成的)JSON 元数据复制回来给自己买到豁免;通过花括号属性夹带进来的 JSON **不打标记**,那部分本来就是手写的,建议对它成立。
|
|
641
|
+
|
|
642
|
+
迁移建议一字未改,JSON 作者面照旧每次模块加载报一次;提示文案现在写明它针对哪一个作者面。
|
|
643
|
+
- a1609a6: Console list filters: a `between` range is submitted only when both bounds are filled, and six operator labels stop rendering as raw i18n keys.
|
|
644
|
+
|
|
645
|
+
Two defects in the list-view filter panel (objectstack#8815), both in the Console
|
|
646
|
+
render layer, with no workaround available downstream.
|
|
647
|
+
|
|
648
|
+
**A half-filled range no longer refuses the whole view.** Picking a date column
|
|
649
|
+
and 「介于」 draws two inputs — that part landed in objectui#3958 — but typing
|
|
650
|
+
only one bound produced `["2024-01-01", ""]`, and both write paths read "is this
|
|
651
|
+
row filled in?" with one shape-blind predicate (`null` / `''` / empty array).
|
|
652
|
+
An array of length 2 passed it, so the empty bound went to the server, which
|
|
653
|
+
refuses the query outright (`400 INVALID_FILTER`): the list showed
|
|
654
|
+
「该视图的查询被拒绝」 and the filters the user had already applied stopped
|
|
655
|
+
applying too. The saved-view fold persisted the same half-range, so the refusal
|
|
656
|
+
came back on every later read of that view, for every user of it.
|
|
657
|
+
|
|
658
|
+
The spec cannot intercept this — `ViewFilterRuleSchema` accepts
|
|
659
|
+
`["2024-01-01", ""]` because it counts the two slots rather than what is in
|
|
660
|
+
them, while refusing a scalar or a one-element array. Authoring validation is
|
|
661
|
+
therefore green on exactly the shape that fails at query time, which makes not
|
|
662
|
+
emitting it the producer's job. `@object-ui/components` now exports
|
|
663
|
+
`isFilterValueComplete(operator, value)` — arity-aware, so a `pair` row needs
|
|
664
|
+
both bounds — and the two consumers that had each kept a copy of the old
|
|
665
|
+
predicate (`plugin-list`'s `convertFilterGroupToAST`, `app-shell`'s
|
|
666
|
+
`foldFilterGroupToSpecRules`) read it instead. A half-filled range is now
|
|
667
|
+
dropped exactly as a half-typed `equals` row already was: no filter, rather than
|
|
668
|
+
a filter the server will reject. Bounds of `0` and `false` stay real bounds.
|
|
669
|
+
|
|
670
|
+
**Six operator labels are translated in all ten locale packs.**
|
|
671
|
+
`startsWith`, `endsWith`, `isNull`, `isNotNull`, `exists` and `notExists` were
|
|
672
|
+
missing from every pack, so i18next resolved them to the raw key and the dropdown
|
|
673
|
+
showed `filterBuilder.operators.isNull` beside translated entries. The
|
|
674
|
+
component's own defaults table could not cover it: that table serves only the
|
|
675
|
+
no-provider path, and the Console mounts a provider. The report named four —
|
|
676
|
+
a `date` column's bucket offers the four nullness operators; a `text` column
|
|
677
|
+
showed all six.
|
|
678
|
+
|
|
679
|
+
Because the label key is built dynamically (`t(\`filterBuilder.operators.${op}\`)`),
|
|
680
|
+
no existing gate could see the gap: the call-site checker classifies a template
|
|
681
|
+
key as `missing-prefix` and only asks whether the prefix resolves, and
|
|
682
|
+
cross-pack parity is satisfied when all ten packs are missing a key together.
|
|
683
|
+
A new parity test pins the packs against `FILTER_BUILDER_OPERATORS` in both
|
|
684
|
+
directions, so an operator added to the dropdown now fails loudly until every
|
|
685
|
+
pack labels it.
|
|
686
|
+
- 53f23bc: `FilterBuilder` shows the falsy values a row actually holds — a boolean `false` and a number `0` are values, not empty boxes.
|
|
687
|
+
|
|
688
|
+
The value controls asked `!condition.value` and `String(condition.value || "")`,
|
|
689
|
+
which folds `false` and `0` in with the rows nobody has filled in yet. Both rows
|
|
690
|
+
saved, persisted and filtered by their value the whole time; only the control
|
|
691
|
+
said otherwise:
|
|
692
|
+
|
|
693
|
+
- a boolean column filtered `equals false` snapped back to the **Select value**
|
|
694
|
+
placeholder the moment the user clicked **False**, while the row carried
|
|
695
|
+
`value: false`;
|
|
696
|
+
- a number column filtered `equals 0` showed an empty box — and typing `0` into
|
|
697
|
+
one looked like the keystroke had never landed, because the row took the value
|
|
698
|
+
and the very next render blanked the input;
|
|
699
|
+
- a single-select whose option id is `0` showed the placeholder too, even though
|
|
700
|
+
the same control's multi-select branch already drew that option as checked.
|
|
701
|
+
|
|
702
|
+
"No value" is now one judgement (`undefined` / `null` / `''`), read by every
|
|
703
|
+
value control and by the two helpers that already spelled it out correctly, so
|
|
704
|
+
"not picked yet" and "picked False" stay two distinguishable states rather than
|
|
705
|
+
trading places.
|
|
706
|
+
|
|
707
|
+
The three keyed numeric paths — the token input's commit, a range bound, and the
|
|
708
|
+
single value input — no longer read with `parseFloat(raw) || 0`, which takes half
|
|
709
|
+
of `"42abc"` and turns `"acme"` into `0`: a filter the user never wrote. All
|
|
710
|
+
three now use the same strict reading a field switch uses, so this component
|
|
711
|
+
holds one answer to "is this string a number" instead of a strict one and a
|
|
712
|
+
lenient one. An unreadable entry becomes an unfilled value, except in the token
|
|
713
|
+
input, which declines the commit and leaves the text in the draft box to be
|
|
714
|
+
fixed. No behaviour a user can reach today changes: those inputs are
|
|
715
|
+
`<input type="number">`, which never hands a non-numeric string to the component
|
|
716
|
+
in the first place — this closes the drift, before a text box, a formula or a
|
|
717
|
+
paste path opens it.
|
|
718
|
+
- c4533dc: `FilterBuilder` settles a row's operator when its **field** changes, instead of leaving an operator the new field's dropdown does not list.
|
|
719
|
+
|
|
720
|
+
The operator buckets are per field type and they do not nest: a `select` column
|
|
721
|
+
offers `in` / `notIn`, a `text` column offers none of them, and only a date
|
|
722
|
+
column offers `between`. Changing a row's field wrote `{ field }` alone, so the
|
|
723
|
+
operator survived into a bucket that no longer contained it. Radix's
|
|
724
|
+
`SelectValue` matches against the `SelectItem`s actually mounted, so the
|
|
725
|
+
operator trigger rendered **blank** — while the row went on filtering by an
|
|
726
|
+
operator the user could neither see nor reach, and could only clear by deleting
|
|
727
|
+
the row.
|
|
728
|
+
|
|
729
|
+
Changing the field is now one edit with the operator and the value's shape, the
|
|
730
|
+
same way objectui#3958 / PR #4762 made changing the operator one edit with the
|
|
731
|
+
value's shape:
|
|
732
|
+
|
|
733
|
+
- an operator the new field's bucket still offers is **kept** — switching
|
|
734
|
+
`contains` from one text column to another must not silently become `equals`,
|
|
735
|
+
and the value it carries is left alone;
|
|
736
|
+
- one the new bucket cannot offer is replaced by that bucket's **first** entry,
|
|
737
|
+
and the row's `value` is then re-shaped for the family it lands in — a list
|
|
738
|
+
under `in` collapses to its first entry under `equals`, a `between` range
|
|
739
|
+
keeps its lower bound, an untouched `[]` becomes `''`.
|
|
740
|
+
|
|
741
|
+
Membership is decided through the spec's own `normalizeFilterOperator`, the fold
|
|
742
|
+
`filterValueArity` already uses, so a stored rule that reaches the builder
|
|
743
|
+
spelled `not_in` is recognised as the operator the dropdown lists as `notIn` and
|
|
744
|
+
is not reset out from under the author. The fold is injective over this
|
|
745
|
+
builder's whole operator vocabulary, which is what makes comparing through it
|
|
746
|
+
safe; a test pins that, and fails the day an added operator would break it.
|
|
747
|
+
- be60815: `FilterBuilder` settles a row's **value** when its field changes, instead of leaving a value the new column's input cannot show.
|
|
748
|
+
|
|
749
|
+
objectui#4768 / PR #4779 settled the row's operator on a field switch and
|
|
750
|
+
re-shaped the value only when the operator's family changed — scalar to scalar
|
|
751
|
+
has no shape question, so what the user typed was carried through on purpose.
|
|
752
|
+
But the field's **type** changed too, and the value input is redrawn from it: a
|
|
753
|
+
browser renders a non-numeric value in `<input type="number">` as **blank**. A
|
|
754
|
+
`text` row filtered `equals "acme"`, pointed at a number column, showed an empty
|
|
755
|
+
box while the row went on carrying `"acme"` — `foldFilterGroupToSpecRules`
|
|
756
|
+
persisted it and the live grid queried `amount equals "acme"`. The same
|
|
757
|
+
invisible-value shape as objectui#4768, one column over.
|
|
758
|
+
|
|
759
|
+
Changing the field is now one edit with the operator, the value's shape **and**
|
|
760
|
+
the value's type. Convertible values are carried, the rest clear to the family's
|
|
761
|
+
empty shape (scalar `''`, list `[]`, range `[]`):
|
|
762
|
+
|
|
763
|
+
- `"42"` on a number column becomes the number `42`; `"acme"`, `"42abc"` and
|
|
764
|
+
`"1,000"` clear. The reading is deliberately stricter than `parseFloat`, which
|
|
765
|
+
would turn `"acme"` into `0` — a filter the user never wrote;
|
|
766
|
+
- `"true"` / `"false"` convert on a boolean column, and a boolean becomes
|
|
767
|
+
`"true"` / `"false"` on a text column, so the round trip closes; `1` and
|
|
768
|
+
`"yes"` are conventions rather than readings, and clear;
|
|
769
|
+
- date-like columns take only what their own input can render, plus the one
|
|
770
|
+
truncation that loses nothing it could have shown (`"2024-03-05T14:30"` →
|
|
771
|
+
`"2024-03-05"` on a date column). A bare date does **not** gain a midnight to
|
|
772
|
+
fit a `datetime` column: `equals 2024-03-05T00:00` is a filter that looks
|
|
773
|
+
answered and matches almost nothing;
|
|
774
|
+
- a value the new column can already hold is left alone — switching between two
|
|
775
|
+
text columns, or two numeric ones, still keeps what the user typed, and an
|
|
776
|
+
unfilled row stays unfilled.
|
|
777
|
+
|
|
778
|
+
The convertibility judgement is defined once, next to `reshapeFilterValue`,
|
|
779
|
+
and `getInputType` now reads the same family table it does — so the type a value
|
|
780
|
+
is converted **to** and the input it is edited **in** cannot drift apart.
|
|
781
|
+
- 37f6844: FilterConditionField can author the spec's `$icontains` — case-insensitive contains is reachable from the filter UI.
|
|
782
|
+
|
|
783
|
+
`@objectstack/spec`'s `FieldOperatorsSchema` gained `$icontains` between
|
|
784
|
+
`17.0.0-rc.2` and `rc.5`, and every driver and evaluation face the platform
|
|
785
|
+
ships now executes it. `FilterConditionField` had no builder operator that could
|
|
786
|
+
author it, so the capability was unreachable from the sharing-rule criteria
|
|
787
|
+
builder and sat in that widget's parity test as an explicit `KNOWN_UNREACHABLE`
|
|
788
|
+
entry.
|
|
789
|
+
|
|
790
|
+
The FilterBuilder gains a `containsCaseInsensitive` operator ("Contains (ignore
|
|
791
|
+
case)", translated in all ten locale packs). `condToMongo` emits
|
|
792
|
+
`{ field: { $icontains: value } }` and `kvToCondition` reads it back, so a saved
|
|
793
|
+
criteria reopens in the visual builder instead of falling into the raw-JSON
|
|
794
|
+
editor. Today's `contains` is unchanged and still emits the case-SENSITIVE
|
|
795
|
+
`$contains`; whether it should have been case-insensitive all along is a product
|
|
796
|
+
question that stays open, and stored filter views keep meaning what they meant.
|
|
797
|
+
|
|
798
|
+
The fold is ASCII-only by contract — `café` does not match `CAFÉ`.
|
|
799
|
+
|
|
800
|
+
The new operator is **opt-in per consumer**: `FilterBuilder` takes an
|
|
801
|
+
`extraOperators` prop, and only `FilterConditionField` passes it. The one
|
|
802
|
+
dropdown feeds three at-rest dialects and only the MongoDB-style criteria this
|
|
803
|
+
widget writes can carry the operator — the spec's `VIEW_FILTER_OPERATORS` (saved
|
|
804
|
+
views) and `VALID_AST_OPERATORS` (the live grid's filter AST) have no
|
|
805
|
+
case-insensitive contains, so offering it there would author a filter those
|
|
806
|
+
paths cannot execute. Every other FilterBuilder is unchanged.
|
|
807
|
+
- 93de4f6: fix(components): FilterBuilder 的 lookup 列不再因 `options: []` 被拒掉远程搜索 (objectui#5031)
|
|
808
|
+
|
|
809
|
+
`renderValueInput` 里那条远程 picker 分支的条件写的是 `!field?.options`,而 `[]`
|
|
810
|
+
是真值 —— 于是一个带 `referenceTo`、`options` 为空数组的 lookup 列**进不到**
|
|
811
|
+
`LookupValuePicker`,落进按 options 画的分支:标量算子得到一个候选数为 0 的
|
|
812
|
+
Select(没有搜索框),`in` / `notIn` 得到一个空的勾选框列表。用户在这一列上挑不出
|
|
813
|
+
任何**新**值,而同一列若 `options` 键干脆缺席反而能拿到完整的远程搜索。可达性不需要
|
|
814
|
+
任何异常状态:`@object-ui/fields` 的 `deriveFilterFields` 与 `plugin-view` 的
|
|
815
|
+
`deriveFieldOptions` 都把 `options` 原样透传,对象元数据里 picklist 值尚未到位时
|
|
816
|
+
就是这个形状。
|
|
817
|
+
|
|
818
|
+
objectui#4874(PR #5030)已经为「静态选项集是否真的在位」建了唯一判据
|
|
819
|
+
`hasStaticOptionDomain(field)`(= `options` 是**非空数组**),并按「`options: []`
|
|
820
|
+
属于远程/未到位」这一侧裁定了值域行为。这条分支条件把同一个问题又答了一遍,两个答案
|
|
821
|
+
对 `options: []` 相反:值域侧当它是远程列(保值),控件侧当它是静态列(画空 Select)。
|
|
822
|
+
|
|
823
|
+
按 2026-08-17 维护者裁定,分支条件改读同一个判据:
|
|
824
|
+
|
|
825
|
+
- `referenceTo`(或 `type` 为 `user` / `owner`)且 `options` 为 `[]` 的 lookup 列
|
|
826
|
+
→ `LookupValuePicker`,与 `options` 键缺席时完全一致:有搜索框、有候选、能选出
|
|
827
|
+
新值;多值算子走 picker 的多选形态,仍然回吐列表(objectui#3958)。
|
|
828
|
+
- `options` **非空**的列不受影响 —— 选项集就是它的全部值域,静态 Select 依旧。
|
|
829
|
+
- 分支的其余条件未动:没有 `referenceTo` 又不是 `user` / `owner` 的列(无处可搜)、
|
|
830
|
+
以及 `select` 这类非 lookup 列,路由与此前逐字相同。
|
|
831
|
+
|
|
832
|
+
「值必须可见」这一条不变,只是由 picker 而不是临时 `SelectItem` 兑现。
|
|
833
|
+
- 2b50261: `FilterBuilder` gives the set and range operators an input that matches the value shape the spec accepts, and stops minting the shape it refuses.
|
|
834
|
+
|
|
835
|
+
Three independent paths let one filter row end up with `operator: 'in'` and a
|
|
836
|
+
SCALAR `value` — the shape `ViewFilterRuleSchema` refuses at save time since
|
|
837
|
+
objectstack#6227, and the shape the query path answered `400 INVALID_FILTER` on
|
|
838
|
+
before that (objectstack#5869):
|
|
839
|
+
|
|
840
|
+
- Changing the operator dropdown wrote `{ operator }` alone, so the seed `''`
|
|
841
|
+
(or whatever the previous family had produced) survived the switch into
|
|
842
|
+
`in` / `not_in` / `between`. The operator and the shape of its value are one
|
|
843
|
+
edit, so they are now made together: switching families re-shapes the value —
|
|
844
|
+
a typed scalar becomes a one-element list, an empty one becomes `[]`, a range
|
|
845
|
+
keeps its first bound and leaves the second open, and a list collapsing to a
|
|
846
|
+
scalar keeps its first entry.
|
|
847
|
+
- A plain text or number column has no static `options`, so `in` fell through to
|
|
848
|
+
the single-value input and the user could only ever type a scalar into it.
|
|
849
|
+
Those columns now get a token input (type, Enter or comma commits, `×` or
|
|
850
|
+
Backspace removes) that always emits an array; `between` gets its two bounds
|
|
851
|
+
instead of one box. The lookup picker's no-DataSource fallback, which also
|
|
852
|
+
handed back a scalar while `multiple`, emits a list too.
|
|
853
|
+
- The multi-value families were decided from a local `["in", "notIn"]` literal,
|
|
854
|
+
already one spelling adrift: `notIn` is an alias and the canonical member is
|
|
855
|
+
`not_in`, so a stored view read back in canonical form got the single-value
|
|
856
|
+
input for a set operator. The families are now read from `@objectstack/spec`'s
|
|
857
|
+
exported `VIEW_FILTER_LIST_VALUE_OPERATORS` / `VIEW_FILTER_PAIR_VALUE_OPERATORS`
|
|
858
|
+
and folded through `normalizeFilterOperator`, so both spellings of one operator
|
|
859
|
+
get one answer and a family the spec widens is picked up without an edit here.
|
|
860
|
+
|
|
861
|
+
`foldFilterGroupToSpecRules` is unchanged and needed no change: it normalizes the
|
|
862
|
+
operator and carries `value` through verbatim, so the shape that reaches storage
|
|
863
|
+
is the producer's to get right. An untouched `in` row arrives as `[]`, which the
|
|
864
|
+
fold's existing incomplete-row rule already drops.
|
|
865
|
+
|
|
866
|
+
Four locale keys are added to all ten packs for the new inputs
|
|
867
|
+
(`filterBuilder.addValue` / `.removeValue` / `.rangeStart` / `.rangeEnd`).
|
|
868
|
+
- 384f30d: fix(components): FilterBuilder 的值不再落在列的选项集之外还看不见 (objectui#4874)
|
|
869
|
+
|
|
870
|
+
带 `options` 的 select/lookup 列,其值控件是一个受控的 Radix Select,而
|
|
871
|
+
`SelectValue` 只认已挂载的 `SelectItem`。于是文本列的 `equals "acme"` 指到
|
|
872
|
+
picklist 列(选项 `won` / `lost`)之后,值控件显示空,行里仍是
|
|
873
|
+
`value: "acme"` —— `foldFilterGroupToSpecRules` 照样持久化、实时网格照样拿
|
|
874
|
+
`stage equals "acme"` 去查。这是 #4768(operator)、#4781(值的类型)之后
|
|
875
|
+
「看不见的值」的第三张脸,成因是**值域**而不是类型:`select` / `lookup` 属于
|
|
876
|
+
文本族,`"acme"` 在类型上装得下,只是不在该列的选项集里。
|
|
877
|
+
|
|
878
|
+
按 2026-08-17 维护者裁定(A + C 组合):
|
|
879
|
+
|
|
880
|
+
- **静态选项列**(`options` 是非空数组,选项集就是该列的全部值域):切换 field
|
|
881
|
+
时做成员判定,不在选项集里的值清到 #4781 的那套空形 —— 标量 `''`、列表逐项
|
|
882
|
+
过滤后 `[]`。列表是**逐项**判定,用户写对的那几项不会被一颗坏项连坐。
|
|
883
|
+
- **远程/异步列**(lookup 远程搜索、`options` 缺席,或 `options: []` 尚未到位):
|
|
884
|
+
值**保留**并**可见** —— 绝不因一份从未声称完整的本地选项集去删一个合法的
|
|
885
|
+
lookup id。Select 把该值挂成一个临时项(标签用值本身,与
|
|
886
|
+
`LookupValuePicker` 对没有 label 的 id 的做法一致),多值列表把它渲染成一行
|
|
887
|
+
已勾选、可取消的条目。
|
|
888
|
+
- 可见性是**无条件**的:无论值是切列带来的、从已存视图读回来的,还是选项集晚到
|
|
889
|
+
才对不上,控件都显示行里真正带着的东西。「行带值、控件空白」这个形态不再存在,
|
|
890
|
+
也不会为了显示去悄悄改写传入的 `value`。
|
|
891
|
+
- ac600e5: A `user` field in a form now receives `dataSource` / `dependentValues` / `dependsOnLabels`, like every other reference field.
|
|
892
|
+
|
|
893
|
+
The form renderer decided which registered widget gets those three props from a
|
|
894
|
+
module-private `DATA_SOURCE_FIELD_TYPES` set, while `@object-ui/core` kept
|
|
895
|
+
`EXPANDABLE_FIELD_TYPES` for the same underlying fact — a field whose stored
|
|
896
|
+
value is a foreign key into another object. The core side's TSDoc claimed to
|
|
897
|
+
mirror the form's set, and it did for 15 days: the form's copy then gained
|
|
898
|
+
`capability-multiselect` (objectui#2403) and the three widget-hint pickers
|
|
899
|
+
`object-ref` / `filter-condition` / `recipient-picker` (objectui#2421) on the
|
|
900
|
+
same day, after which the two sets were not in a subset relation in either
|
|
901
|
+
direction — `user` only in core, the picker names only in the form — with
|
|
902
|
+
nothing able to report it.
|
|
903
|
+
|
|
904
|
+
The form now derives its rule instead of restating it: the reference half is
|
|
905
|
+
core's set, the form-specific half is the three picker names, which are widget
|
|
906
|
+
hints and can never be a declarable field `type`. Adding a member to
|
|
907
|
+
`EXPANDABLE_FIELD_TYPES` therefore also grants it the form's data-source wiring;
|
|
908
|
+
that coupling is intended and is now written down on both sides.
|
|
909
|
+
|
|
910
|
+
The user-visible half is `user`. It previously received none of the three props.
|
|
911
|
+
`dataSource` and `dependentValues` each have a `SchemaRendererContext` fallback
|
|
912
|
+
inside the widget, so the person picker limped along wherever a provider
|
|
913
|
+
happened to supply one; `dependsOnLabels` has no fallback, so a
|
|
914
|
+
dependency-gated user picker interpolated the raw API name into its
|
|
915
|
+
"select ... first" hint in every locale — the leak objectstack#5407 closed for
|
|
916
|
+
lookups and left open here. The widget contract's own `dataSource` doc has
|
|
917
|
+
always named `user` among the types the form renderer injects for.
|
|
918
|
+
|
|
919
|
+
No change to what is expanded, projected or rendered anywhere else: the core
|
|
920
|
+
set's members are untouched.
|
|
921
|
+
- 97fba31: The built-in form's `default` fallback branch now enforces a declared `max_length` ceiling.
|
|
922
|
+
|
|
923
|
+
The last arm of the field switch — the one serving a `type` that is neither a
|
|
924
|
+
built-in field type nor resolvable from the registry — spread its props straight
|
|
925
|
+
onto the rendered `Input` and never read the declared ceiling. One declaration
|
|
926
|
+
therefore split into two outcomes depending on how it was spelled. Measured on
|
|
927
|
+
`main` after objectui#5201 landed:
|
|
928
|
+
|
|
929
|
+
max_length: 50 -> attrs=["class","max_length",...] maxlength=null
|
|
930
|
+
maxLength: 50 -> attrs=["class","maxlength",...] maxlength="50"
|
|
931
|
+
|
|
932
|
+
The camelCase spelling capped by coincidence — it happens to name a real DOM
|
|
933
|
+
attribute. The legacy `max_length` capped nothing at all and landed as a stray,
|
|
934
|
+
inert `max_length="50"` attribute: invalid HTML that reads like a working cap to
|
|
935
|
+
the next reader. Two independent defects, both now fixed.
|
|
936
|
+
|
|
937
|
+
`max_length` is a live authoring spelling, not a fossil: the registered `field:*`
|
|
938
|
+
widgets have dual-read `maxLength ?? max_length` since framework#1878 §3, all
|
|
939
|
+
three producers of a form field normalize it (`ObjectForm`, `sectionFields`,
|
|
940
|
+
`EmbeddableForm.applyDefaultMaxLengths`), and `@object-ui/types` declares it on
|
|
941
|
+
several field types. This branch serves a hand-authored `FormSchema` handed
|
|
942
|
+
straight to the renderer, where there is no normalizing producer in between and
|
|
943
|
+
the author is the producer — so it was the one reader in the repo that dropped
|
|
944
|
+
the declaration.
|
|
945
|
+
|
|
946
|
+
Same defect and same fix shape as objectui#5201 (the `input` arm) and
|
|
947
|
+
objectui#3439 (the `textarea` arm): the ceiling is resolved locally inside the
|
|
948
|
+
branch, and the legacy key is destructured off locally. The shared
|
|
949
|
+
renderer-only strip table is deliberately unchanged — it feeds the `checkbox`,
|
|
950
|
+
`switch` and `select` arms too, and widening it would alter branches this change
|
|
951
|
+
does not test.
|
|
952
|
+
|
|
953
|
+
A field that declares no ceiling in either spelling renders no `maxlength`
|
|
954
|
+
attribute, exactly as before.
|
|
955
|
+
- 3fbbea1: `container`: honour a declared `maxWidth: false` as "no maximum width"
|
|
956
|
+
|
|
957
|
+
`ContainerSchema` has always declared `maxWidth?: … | false`, but the renderer
|
|
958
|
+
read it as `schema.maxWidth || 'xl'`, so `false` folded into the default and a
|
|
959
|
+
container asking for **no** constraint rendered `max-w-xl` (`max-width: 36rem`)
|
|
960
|
+
— the opposite of what it declared. The `false` arm of the union had no
|
|
961
|
+
reachable path from the day it was declared.
|
|
962
|
+
|
|
963
|
+
`maxWidth` is now read with `??` (the spelling `stack` / `grid` always used, and
|
|
964
|
+
the one objectui#4003 gave this file's `padding` and `flex`'s `gap`), and `false`
|
|
965
|
+
emits an explicit **`max-w-none`** rather than simply omitting a class. The two
|
|
966
|
+
are not the same fact: an omitted class leaves an inherited max-width standing —
|
|
967
|
+
`@tailwindcss/typography`'s `.prose` sets `max-width: 65ch` — while `max-w-none`
|
|
968
|
+
cancels it. The registry `inputs` enum for `maxWidth` gains `false` to match the
|
|
969
|
+
type it lagged, so the designer and `sdui-parser`'s manifest gate stop reporting
|
|
970
|
+
a legal value as `invalid-enum`.
|
|
971
|
+
|
|
972
|
+
No authored node in the repo declared `maxWidth: false`, so nothing that renders
|
|
973
|
+
today changes.
|
|
974
|
+
- 616a2a5: The list filter builder no longer offers `Is set` / `Is not set`, which its query dialects cannot express.
|
|
975
|
+
|
|
976
|
+
**User-visible before/after.** The operator dropdown in the list toolbar's
|
|
977
|
+
filter popover — and in the Studio view/tab/page filter inspectors, the dataset
|
|
978
|
+
inspector and the generic `filter` config field — loses two rows: **"Is set"**
|
|
979
|
+
and **"Is not set"**. The sharing-rule criteria builder (`FilterConditionField`)
|
|
980
|
+
keeps both, unchanged. `Is null` / `Is not null` and `Is empty` / `Is not empty`
|
|
981
|
+
are untouched everywhere and remain the way to filter on a missing value from
|
|
982
|
+
the list.
|
|
983
|
+
|
|
984
|
+
Nothing that worked stops working. Every save path behind those two rows was
|
|
985
|
+
already broken, in three different ways depending on the surface:
|
|
986
|
+
|
|
987
|
+
- **Live grid** — `ListView.mapOperator` had no row for either id, so its
|
|
988
|
+
`default:` arm returned the id verbatim and the query went out as
|
|
989
|
+
`['name', 'exists', 'x']`. `exists` is not a member of the spec's
|
|
990
|
+
`VALID_AST_OPERATORS`, so `isFilterAST()` rejects the shape: an unfiltered
|
|
991
|
+
read or a 400, never the filter the user asked for.
|
|
992
|
+
- **Save as view** — `foldFilterGroupToSpecRules` normalizes through the spec's
|
|
993
|
+
`normalizeFilterOperator`, which does not know the pair, and
|
|
994
|
+
`ViewFilterRuleSchema`'s enum then refuses the rule.
|
|
995
|
+
- **Dataset inspector** — `groupToCondition` has no row either and drops the
|
|
996
|
+
condition silently, so the filter simply never applied.
|
|
997
|
+
|
|
998
|
+
**Why withheld rather than mapped.** Measured on `@objectstack/spec`
|
|
999
|
+
17.0.0-rc.6: neither `VIEW_FILTER_OPERATORS` nor `VALID_AST_OPERATORS` contains
|
|
1000
|
+
an existence operator, under any spelling — both sets have zero members matching
|
|
1001
|
+
`/exist/`. Only the MongoDB-style `FieldOperatorsSchema` criteria carries
|
|
1002
|
+
`$exists`, and that is precisely the dialect `FilterConditionField` writes, so
|
|
1003
|
+
the pair moves behind the existing `OPT_IN_OPERATORS` gate and that widget opts
|
|
1004
|
+
in. Collapsing them onto `isNotNull` / `isNull` was rejected: the builder
|
|
1005
|
+
already draws those as their own rows, the round trip is lossy (a saved
|
|
1006
|
+
`exists` reads back as `isNotNull`), and the spec's own note records `$exists` =
|
|
1007
|
+
has-value as still unsettled across drivers — `driver-memory`'s live mingo path
|
|
1008
|
+
and `driver-mongodb` read key-presence.
|
|
1009
|
+
|
|
1010
|
+
**The class is now closed by an assertion, not by discipline.** objectui's three
|
|
1011
|
+
existing operator-parity guards all sweep spec vocabulary → objectui; none asked
|
|
1012
|
+
whether an id the dropdown draws is an id the consumer can persist, which is the
|
|
1013
|
+
direction that broke. `plugin-list`'s new
|
|
1014
|
+
`list-offered-operator-expressible-parity.test.ts` forces the set the list
|
|
1015
|
+
toolbar offers to **equal** the set its two dialects can express, in both
|
|
1016
|
+
directions — so an unexpressible operator cannot be offered, and an operator
|
|
1017
|
+
that becomes expressible upstream cannot stay needlessly withheld.
|
|
1018
|
+
- 4a0bd17: `page:accordion` now renders an item's `icon` in its panel trigger.
|
|
1019
|
+
|
|
1020
|
+
`PageAccordionItem` (`packages/components/src/renderers/layout/containers.tsx`)
|
|
1021
|
+
has always declared `icon?: string`, but `PageAccordionRenderer` never read it
|
|
1022
|
+
— an authored icon reached the trigger and was silently dropped. The
|
|
1023
|
+
`objectstack` spec's `PageAccordionProps.items[].icon` already treats this as
|
|
1024
|
+
legitimate, undeprecated authorable surface (unlike the neighboring `value`
|
|
1025
|
+
key, which the same schema explicitly flags as dead), so the renderer was the
|
|
1026
|
+
side out of sync. It now renders the Lucide icon before the panel label,
|
|
1027
|
+
following the same convention `page:tabs` items already use in this file.
|
|
1028
|
+
- b8b9af4: `page:header`'s `maxVisible` / `mobileMaxVisible` now honour the contract's value domain instead of a laxer renderer tolerance.
|
|
1029
|
+
|
|
1030
|
+
Three authorities gave two answers for the same value (objectui#5006). Measured on
|
|
1031
|
+
`ComponentPropsMap['page:header']` at `@objectstack/spec@17.0.0` — the member lives
|
|
1032
|
+
on the `@objectstack/spec/ui` subpath, not the package root — both keys are a
|
|
1033
|
+
POSITIVE SAFE INTEGER (`{format:'safeint'}` plus
|
|
1034
|
+
`{check:'greater_than',value:0,inclusive:false}`). Spec rejects `0`, `-1`, `1.5`
|
|
1035
|
+
and anything past `Number.MAX_SAFE_INTEGER`. objectui's manifest gate and
|
|
1036
|
+
`sdui-parser`'s `checkType` said nothing about any of them, and the renderer's
|
|
1037
|
+
`readMax` was looser still: it accepted `0` and floored fractions. So the loosest
|
|
1038
|
+
of the three layers decided what shipped on screen, while `os validate` / `os build`
|
|
1039
|
+
rejected the very same metadata outright.
|
|
1040
|
+
|
|
1041
|
+
`readMax` now accepts only what the contract accepts. `Number.isSafeInteger(v) && v > 0`
|
|
1042
|
+
is the exact translation of `safeint`, not an approximation — plain `Number.isInteger`
|
|
1043
|
+
would admit `2**53 + 2` and `1e21`, which spec rejects.
|
|
1044
|
+
|
|
1045
|
+
Behaviour change, stated because this NARROWS the renderer's accept set rather than
|
|
1046
|
+
only fixing a fault: a contract-rejected value no longer takes effect and falls back
|
|
1047
|
+
to the documented default (3 desktop / 1 mobile). Concretely, `maxVisible: 0` used to
|
|
1048
|
+
render zero inline buttons and sweep every action into the overflow menu, and
|
|
1049
|
+
`maxVisible: 1.5` used to be floored to `1`; both now render the default 3-inline
|
|
1050
|
+
split. This is a narrowing *toward* an already-published contract — no in-tree
|
|
1051
|
+
producer writes a rejected value, so nothing in the repo changes behaviour. Both
|
|
1052
|
+
schema-level and `properties.*` spellings go through the one reader. `action:bar`'s
|
|
1053
|
+
`maxVisible` is an unrelated reader with no `ComponentPropsMap` entry and is
|
|
1054
|
+
deliberately untouched.
|
|
1055
|
+
|
|
1056
|
+
`ComponentInput.type`'s doc comment now records the trade the ruling fixed in place
|
|
1057
|
+
(maintainer, 2026-08-17): the coarse `number` arm plus `description` is the
|
|
1058
|
+
publication face's expression ceiling today, and spec is the sole judge of values.
|
|
1059
|
+
Giving `ComponentInput` real constraint slots, and binding `checkType` to spec, were
|
|
1060
|
+
both deferred with a named reopen condition — a measured case of an author shipping
|
|
1061
|
+
a spec-rejected value that objectui's silence let through.
|
|
1062
|
+
- aff10e2: A `field:`-prefixed `password` no longer renders as clear text when its widget is not registered
|
|
1063
|
+
|
|
1064
|
+
On the built-in path (`@object-ui/fields` not registered) a form field spelled with the
|
|
1065
|
+
`field:`-prefixed widget id resolved nothing and took the form renderer's `default` input
|
|
1066
|
+
branch. That branch's native-input table was keyed on the raw `type`, so the prefixed
|
|
1067
|
+
spelling missed it and rendered `type="text"` — and `mapFieldTypeToFormType` emits the
|
|
1068
|
+
prefixed id for **every** object-derived form, so this was the normal path, not an edge
|
|
1069
|
+
case. An object-derived `password` field therefore put the secret on screen in clear text,
|
|
1070
|
+
and an object-derived `email` field lost its native keyboard and validation.
|
|
1071
|
+
|
|
1072
|
+
The two spellings now get the answer each deserves:
|
|
1073
|
+
|
|
1074
|
+
- **`field:password` refuses.** Reaching the unregistered default with a registry key
|
|
1075
|
+
proves the app shipped without the widget it declares, so the value is not rendered at
|
|
1076
|
+
all — no input, nothing carrying the secret in the DOM. In its place is an inline
|
|
1077
|
+
`role="alert"` refusal naming the missing widget, plus a `console.error` that doubles as
|
|
1078
|
+
the fix instruction. Masking alone would still invite a user to type a secret into a form
|
|
1079
|
+
whose password widget is absent.
|
|
1080
|
+
- **`field:email` renders the native email input**, because the native-input table is now
|
|
1081
|
+
keyed on the declared type with the `field:` prefix stripped.
|
|
1082
|
+
- **The bare `password` / `email` spellings are unchanged.** They claim no registered
|
|
1083
|
+
widget, the default branch is their intended home, and their native input stays exactly
|
|
1084
|
+
as it was.
|
|
1085
|
+
|
|
1086
|
+
Deliberately narrow: only `password` refuses, and only under the `field:` namespace. Every
|
|
1087
|
+
other unregistered `field:*` id renders the same text box it rendered before, and a
|
|
1088
|
+
registered `field:password` widget still wins.
|
|
1089
|
+
- 7458a41: A readonly field's replacement display is now named by the field's label and described by its help text.
|
|
1090
|
+
|
|
1091
|
+
A registered field widget's readonly branch renders a replacement display — a
|
|
1092
|
+
`mailto:` anchor, a formatted span, a chip row, a preview table — and returns
|
|
1093
|
+
before its DOM pass-through, so nothing the form renderer handed down reached an
|
|
1094
|
+
element. Measured on a real form, one field per row, with `description` set: the
|
|
1095
|
+
host control id (`…-form-item`) was on NO element in the document, so the visible
|
|
1096
|
+
label's `for` pointed at nothing and the readonly surface had NO accessible name
|
|
1097
|
+
at all, while the rendered help text had zero consumers. All 34 registered
|
|
1098
|
+
non-group-labelled widget types read identically, including the four display-only
|
|
1099
|
+
ones (`formula` / `summary` / `auto_number` / `vector`) whose whole widget is a
|
|
1100
|
+
replacement display.
|
|
1101
|
+
|
|
1102
|
+
The form renderer now wraps a readonly registered field widget's output in a
|
|
1103
|
+
container carrying the host id, `role="group"`, `aria-labelledby` and
|
|
1104
|
+
`aria-describedby`, and the label publishes an `id` in place of its `for` — the
|
|
1105
|
+
same WAI-ARIA group pattern objectui#3961 / #3990 / #4005 established for the
|
|
1106
|
+
seven composite widgets, applied at the host instead of in each widget. Not one
|
|
1107
|
+
widget file changed: the mechanism lands once, so the current widgets and any
|
|
1108
|
+
future third-party one are correct by construction, with no "remember to spread
|
|
1109
|
+
the host props" step left to miss.
|
|
1110
|
+
|
|
1111
|
+
The name is composite — the label's id AND the container's own — so the VALUE
|
|
1112
|
+
stays in the accessible name (`Email user@example.com`, not just `Email`);
|
|
1113
|
+
`group` is not a name-from-content role, and the value is usually the only thing
|
|
1114
|
+
on screen. `aria-invalid` is deliberately dropped at this boundary: it is
|
|
1115
|
+
control-channel state reporting what a user's own editing may do wrong, and a
|
|
1116
|
+
readonly display cannot be edited (objectui#3291 / #3318 / #4005).
|
|
1117
|
+
|
|
1118
|
+
Two consequences worth stating. Readonly registered fields gain one DOM layer,
|
|
1119
|
+
which end-to-end selectors written against the widget root as a direct child of
|
|
1120
|
+
the form item will see; the layer carries `data-slot="readonly-field-group"` as a
|
|
1121
|
+
stable locator. And because that layer is a block box where several readonly
|
|
1122
|
+
faces were inline, those rows now take the form's standard label-to-value
|
|
1123
|
+
spacing, matching the editable state. Builtin types (`input` / `textarea` /
|
|
1124
|
+
`checkbox` / `switch` / `select`), editable fields, group-labelled widgets and
|
|
1125
|
+
fields rendered without a label are untouched, byte for byte.
|
|
1126
|
+
- d971e51: A create form no longer deadlocks on a `requiredWhen` field that also declares a runtime `defaultValue`.
|
|
1127
|
+
|
|
1128
|
+
`#4069` ruled that in **create** mode a field whose `defaultValue` is a runtime
|
|
1129
|
+
instruction the server resolves per insert (`NOW()` / `current_user`, or a CEL
|
|
1130
|
+
Expression envelope) is producer-owned: the control is deliberately left empty
|
|
1131
|
+
and the key is omitted from the payload, because `ObjectQL.applyFieldDefaults`
|
|
1132
|
+
resolves the declaration only for a field that arrives absent or null. That was
|
|
1133
|
+
implemented on the STATIC `required` flag.
|
|
1134
|
+
|
|
1135
|
+
The conditional spelling was not covered. `requiredWhen` is resolved one layer
|
|
1136
|
+
downstream, in the form renderer, against the live record — so a predicate
|
|
1137
|
+
resolving TRUE on a create form put the requirement straight back: the control
|
|
1138
|
+
was still empty by design, the submit was refused, and the user had nothing
|
|
1139
|
+
sensible to type.
|
|
1140
|
+
|
|
1141
|
+
Both spellings now behave identically on a producer-owned field. A
|
|
1142
|
+
`requiredWhen` predicate is a claim about the value at rest in a given state,
|
|
1143
|
+
and `NOW()` / `current_user` resolve at insert regardless of state, so the
|
|
1144
|
+
producer's guarantee covers the conditional claim by the same argument that
|
|
1145
|
+
covers the unconditional one. An author who really means "the user must supply
|
|
1146
|
+
this in this state" has a natural spelling for it: do not declare the default.
|
|
1147
|
+
|
|
1148
|
+
The suppression lands in the single evaluator both layers read,
|
|
1149
|
+
`resolveFieldRuleState` — the same verdict that draws the required marker and
|
|
1150
|
+
the one the submit-time check consults — so a field can never lose its asterisk
|
|
1151
|
+
while still refusing the write. The classifier that answers "is this value the
|
|
1152
|
+
producer's to supply" moved down to `@object-ui/core`
|
|
1153
|
+
(`isRuntimeDefault` / `isServerOwnedValue`, re-exported from
|
|
1154
|
+
`@object-ui/plugin-form`) so the renderer, the wizard's cross-step gate and the
|
|
1155
|
+
create-form field builders all read one implementation rather than three.
|
|
1156
|
+
|
|
1157
|
+
**Edit mode is unchanged.** Defaults do not re-apply to an existing record, so
|
|
1158
|
+
on a persisted row the token was already resolved at insert and blanking the
|
|
1159
|
+
column is a real removal: `requiredWhen` enforces there exactly as authored.
|
|
1160
|
+
Fields with no declared default, and fields whose default is a static literal
|
|
1161
|
+
(which IS seeded into the control), are also unaffected in both modes.
|
|
1162
|
+
- 75444e3: The dependency-gate hint now enumerates its controlling fields with the locale's
|
|
1163
|
+
own list separator, and reads identically whichever caller produced it.
|
|
1164
|
+
|
|
1165
|
+
`lookup.selectFirst` and `fields.options.selectFirst` are deliberately one
|
|
1166
|
+
wording, so a field gated on two or more parents says the same thing whether the
|
|
1167
|
+
lookup widget or the form renderer rendered it. The sentence was shared but its
|
|
1168
|
+
`{{fields}}` slot was not: each call site joined the controlling-field names
|
|
1169
|
+
with its own hardcoded separator, and not even the same one — `', '` in
|
|
1170
|
+
`LookupField`, `' / '` in the form renderer's `gatedHint` and in
|
|
1171
|
+
`OptionsEmptyState`. A field gated on Account and Lead Source read
|
|
1172
|
+
`Select Account, Lead Source first` from one side and
|
|
1173
|
+
`Select Account / Lead Source first` from the other.
|
|
1174
|
+
|
|
1175
|
+
A list separator is a property of the locale rather than of the code, so both
|
|
1176
|
+
spellings were also wrong for the script under zh/ja (which enumerate with
|
|
1177
|
+
U+3001) and under ar (U+060C). All three call sites now read
|
|
1178
|
+
`validation.formInvalidJoiner` — the key already shipped in all ten packs for
|
|
1179
|
+
the invalid-submit toast's field list, which is the same class of truncated-name
|
|
1180
|
+
list. One key, every caller: a second, gate-specific key would have recreated
|
|
1181
|
+
the divergence the shared sentence exists to prevent.
|
|
1182
|
+
|
|
1183
|
+
No locale pack changes, and no change to what a provider-less render produces in
|
|
1184
|
+
English: the `@object-ui/fields` defaults table declares the joiner as `', '`,
|
|
1185
|
+
the `en` pack's value and the literal `LookupField` previously hardcoded.
|
|
1186
|
+
- 2d0bd16: Remove two unreachable renderer registrations, and fail the build on any new same-namespace duplicate.
|
|
1187
|
+
|
|
1188
|
+
The component registry silently keeps the LAST registration for a given
|
|
1189
|
+
`namespace:type` key. Two renderers on `main` were therefore dead code — they
|
|
1190
|
+
compiled, type-checked, and never ran:
|
|
1191
|
+
|
|
1192
|
+
- `renderers/data-display/table.tsx` (`SimpleTableRenderer`) lost `ui:table` to
|
|
1193
|
+
`renderers/complex/table.tsx`, because `renderers/index.ts` imports
|
|
1194
|
+
`./data-display` before `./complex`. It was the only table renderer that read
|
|
1195
|
+
`bind`, which is why a `table` node with a two-row `bind` rendered a header and
|
|
1196
|
+
zero rows (objectui#5125).
|
|
1197
|
+
- The `kbd` entry in `renderers/basic/html-elements.tsx`'s `TAGS` loop lost
|
|
1198
|
+
`ui:kbd` to `renderers/data-display/kbd.tsx` — despite that list's own comment
|
|
1199
|
+
stating it excludes anything already registered.
|
|
1200
|
+
|
|
1201
|
+
In both cases the renderer that serves the key today is the one kept, so no
|
|
1202
|
+
reachable behaviour changes: `table` still renders inline `data` against
|
|
1203
|
+
`columns` and still ignores `bind`, and `kbd` still renders one `<kbd>` per entry
|
|
1204
|
+
in `keys`. Both readings are now pinned by tests.
|
|
1205
|
+
|
|
1206
|
+
Whether `table` *should* read `bind` is deliberately left open — that would widen
|
|
1207
|
+
the authorable key surface and is a product decision, not a consequence of
|
|
1208
|
+
deleting dead code.
|
|
1209
|
+
|
|
1210
|
+
The new gate (`renderers/__tests__/registration-uniqueness.test.tsx`) counts every
|
|
1211
|
+
registration the production barrel makes and fails on any key registered twice
|
|
1212
|
+
under one namespace, so a re-introduced duplicate is caught at CI rather than by
|
|
1213
|
+
an accidental probe. It is a test rather than a runtime warning because
|
|
1214
|
+
re-registering a key is a supported pattern for test stubs, and a warning there
|
|
1215
|
+
would fire mostly on legitimate overrides.
|
|
1216
|
+
- dad51e5: fix(fields): deliver the host's a11y channels to `slider` and name `signature`
|
|
1217
|
+
|
|
1218
|
+
`SliderField` and `SignatureField` forwarded nothing a form host handed them —
|
|
1219
|
+
neither spread `toDomProps(props)` at all — so `<FormControl>`'s whole payload
|
|
1220
|
+
landed on nothing. Measured on a real form, one required field per row, freshly
|
|
1221
|
+
failed validation:
|
|
1222
|
+
|
|
1223
|
+
```
|
|
1224
|
+
slider ariaInvalidTrue=[] labelFor=…-form-item -> DANGLING descConsumers=0 ids=[]
|
|
1225
|
+
signature ariaInvalidTrue=[] labelFor=…-form-item -> DANGLING descConsumers=0 ids=[]
|
|
1226
|
+
text ariaInvalidTrue=[input] labelFor -> input descConsumers=1
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
`ids=[]` is the tell: no element in either row carried an id at all, so the
|
|
1230
|
+
visible label pointed `for` at nothing, the rendered help text had zero
|
|
1231
|
+
consumers, and a failed slider announced no error state.
|
|
1232
|
+
|
|
1233
|
+
**`slider`** now delivers all three. Its focusable control is Radix's
|
|
1234
|
+
`span[role="slider"]` thumb, which the synced `ui/slider.tsx` renders internally
|
|
1235
|
+
and does not export, so the primitive grew a declared `thumbProps` — routed
|
|
1236
|
+
through a new `lib/slider-thumb` and applied to the no-touch file as a declared
|
|
1237
|
+
sync patch, so it survives regeneration. The split of which keys stay on Root
|
|
1238
|
+
(`name`, `disabled`) is the one the `select` fix already settled.
|
|
1239
|
+
|
|
1240
|
+
**`signature`** gets the name and the description on a `role="group"` container.
|
|
1241
|
+
Its control state deliberately does not follow: the drawing surface is a
|
|
1242
|
+
`<canvas>` with no keyboard path, and its only other element is disabled while
|
|
1243
|
+
the pad is empty, so there is no element a control state could be read from.
|
|
1244
|
+
|
|
1245
|
+
Both are now declared `labelling: 'group'` — a `<span>` and a `<canvas>` are not
|
|
1246
|
+
labelable elements, so a host `for` could only dangle at them.
|
|
1247
|
+
- 1c9c342: The `span` deprecation notice is now reported once per page load, and only to the authoring surface it applies to.
|
|
1248
|
+
|
|
1249
|
+
`SpanRenderer` was two rulings behind `div`. It still `console.warn`ed on **every
|
|
1250
|
+
render**, and it still fired at nodes the `kind:'html'` tier's own parser had
|
|
1251
|
+
emitted — the two defects that were ruled on for `div` in objectui#3965 (PR
|
|
1252
|
+
#3998, which explicitly named `span` as the follow-up) and objectui#4000 (PR
|
|
1253
|
+
#4916, which built the provenance mechanism). This is that follow-up
|
|
1254
|
+
(objectui#4917); it copies the shape now in `basic/div.tsx` rather than inventing
|
|
1255
|
+
a second one.
|
|
1256
|
+
|
|
1257
|
+
- **Once per module load.** The notice is a property of the deprecated TYPE, not
|
|
1258
|
+
of each node, so repeating it per render only buries the page's real console
|
|
1259
|
+
errors. The deprecation still fires in dev builds, exactly once.
|
|
1260
|
+
- **JSON-authored nodes only.** An author writing the plain inline tag in a
|
|
1261
|
+
`kind:'html'` page gets a node this deprecated renderer serves — and was told
|
|
1262
|
+
to migrate to `badge` / `text`, neither of which exists in that tier's
|
|
1263
|
+
vocabulary, with nothing they could write to make it stop. Provenance is
|
|
1264
|
+
established by the producer (the parser stamps what it emits, via
|
|
1265
|
+
`isHtmlTierNode`), not guessed from the node's shape here.
|
|
1266
|
+
- **The notice now names its surface**, so whoever reads the console can tell
|
|
1267
|
+
which of their pages it is about. The migration guidance itself is unchanged:
|
|
1268
|
+
this narrows WHO is told, it does not water down WHAT they are told.
|
|
1269
|
+
|
|
1270
|
+
Order is load-bearing and pinned by tests: the html-tier exemption is checked
|
|
1271
|
+
BEFORE the warn-once set is marked, so an html-tier node rendering first cannot
|
|
1272
|
+
swallow the notice a JSON-authored node earns later on the same page.
|
|
1273
|
+
- 787c738: `span` renders the `value` its type and its published doc both declare, with child content winning when both are present.
|
|
1274
|
+
|
|
1275
|
+
Two declared authoring surfaces named `value` the text content of a span and the
|
|
1276
|
+
renderer read neither: `TextSpanSchema` declares `value?: string` commented
|
|
1277
|
+
`Text content` (`packages/types/src/layout.ts`), and the published doc's Schema
|
|
1278
|
+
block lists the same key with the same comment. Before objectui#5027 the
|
|
1279
|
+
renderer read `body`, which no producer emits; #5027 moved it to the canonical
|
|
1280
|
+
`children`. Neither version ever read `value`. So an author writing
|
|
1281
|
+
`{ "type": "span", "value": "hello" }` — exactly what the type and the docs
|
|
1282
|
+
instruct — got an empty element, with no warning and no diagnostic. Same failure
|
|
1283
|
+
shape as #5027 (content silently dropped), one key over, and not catchable on
|
|
1284
|
+
the type surface: `BaseSchema` carries an index signature, so no spelling on
|
|
1285
|
+
this node is ever a TS error.
|
|
1286
|
+
|
|
1287
|
+
Precedence, ruled 2026-08-17: `children` wins, `value` is the fallback. This is
|
|
1288
|
+
the shape the sibling type in the same family already sets — `basic/text.tsx`
|
|
1289
|
+
renders `schema.content || schema.value` — so `span` stops being the odd one out
|
|
1290
|
+
rather than growing a rule of its own. "No child content" means an absent, empty
|
|
1291
|
+
or empty-array `children`; that is exactly when `value` renders.
|
|
1292
|
+
|
|
1293
|
+
Both declared faces stay as written. The fix makes them true instead of
|
|
1294
|
+
retracting a key that has been published as authorable. `body` remains refused
|
|
1295
|
+
(objectui#5027): it is declared nowhere for this type, whereas `value` is
|
|
1296
|
+
declared twice — the question is whether a key was published as authorable, not
|
|
1297
|
+
whether a tolerant read would be convenient.
|
|
1298
|
+
- 8396656: The `span` renderer renders its content again — it reads `children`, the key its own type declares and its producers emit.
|
|
1299
|
+
|
|
1300
|
+
The renderer read `schema.body` and nothing else, and no producer on either of
|
|
1301
|
+
its authoring surfaces emits that key. In a `kind:'html'` page the parser
|
|
1302
|
+
assigns compiled child nodes to `children`
|
|
1303
|
+
(`@object-ui/sdui-parser`'s `parse.ts`), so an author writing the plain inline
|
|
1304
|
+
tag with text inside it got an EMPTY element back — the text was dropped with no
|
|
1305
|
+
warning and no diagnostic, because the parser's tree validation does not inspect
|
|
1306
|
+
child keys. A sibling paragraph on the same page rendered normally, which is what
|
|
1307
|
+
made this read as anything but a compile failure. On the JSON surface the same
|
|
1308
|
+
thing happened to anyone following the declaration: `TextSpanSchema` declares
|
|
1309
|
+
`value` and `children`, so `children` is what an author writes, and `children` is
|
|
1310
|
+
what rendered nothing.
|
|
1311
|
+
|
|
1312
|
+
The canonical child key is `children` — what the type declares, what the parser
|
|
1313
|
+
emits, and what the sibling `div` renderer already reads. `body` is deliberately
|
|
1314
|
+
NOT accepted as a second spelling: a tolerant read would fossilize a second
|
|
1315
|
+
de-facto contract for the one type whose declaration never named it
|
|
1316
|
+
(Commandment #0.1), and a repo sweep found no page, example, catalog entry or
|
|
1317
|
+
metadata document authoring it on this tag. A pin test states both halves — the
|
|
1318
|
+
content renders, and a `body` alias renders nothing — so re-adding the lenient
|
|
1319
|
+
read turns a test red rather than passing review.
|
|
1320
|
+
|
|
1321
|
+
Reachability, stated plainly: the `span` type is deprecated for JSON-authored
|
|
1322
|
+
pages, but it is permanent first-class vocabulary of the `kind:'html'` tier,
|
|
1323
|
+
where the tag is compiled straight through and no other spelling exists. So the
|
|
1324
|
+
authors who could do nothing about the deprecation were exactly the ones losing
|
|
1325
|
+
their text.
|
|
1326
|
+
|
|
1327
|
+
Not changed here, and named because the declaration still promises it: `value`
|
|
1328
|
+
on this tag is declared by `TextSpanSchema` and read by nobody, as it was before
|
|
1329
|
+
this fix. Making it render needs a ruling on precedence against `children`,
|
|
1330
|
+
which belongs with the wider child-key drift (objectui#4631) rather than in a
|
|
1331
|
+
rendering-path fix.
|
|
1332
|
+
- 138ab04: Fix a list filter that silently applied nothing when the first thing you picked was **Is null** or **Is not null**.
|
|
1333
|
+
|
|
1334
|
+
Adding a filter seeds the row with no value, and changing the operator keeps it that way — which is correct for an operator that takes no value, since the panel draws no input for one. The live grid read that row as unfinished and dropped it: the filter appeared in the panel, the query went out without it, and every record came back with no error to explain it. Only `Is empty` / `Is not empty` were exempt; the builder renders six operators value-less.
|
|
1335
|
+
|
|
1336
|
+
Which operators are complete without a value now has a single owner — `VALUELESS_FILTER_BUILDER_OPERATORS`, exported from `@object-ui/components` beside the code that decides it. The live grid and the saved-view fold both read it instead of keeping their own lists, so the two halves of one interaction can no longer disagree about whether a row is finished.
|
|
1337
|
+
- Updated dependencies [88085e3]
|
|
1338
|
+
- Updated dependencies [69251bf]
|
|
1339
|
+
- Updated dependencies [57e668f]
|
|
1340
|
+
- Updated dependencies [516663d]
|
|
1341
|
+
- Updated dependencies [41ac1b7]
|
|
1342
|
+
- Updated dependencies [1eaf0a1]
|
|
1343
|
+
- Updated dependencies [2533ec5]
|
|
1344
|
+
- Updated dependencies [bbe8b86]
|
|
1345
|
+
- Updated dependencies [8477be5]
|
|
1346
|
+
- Updated dependencies [279fb13]
|
|
1347
|
+
- Updated dependencies [2e82ab2]
|
|
1348
|
+
- Updated dependencies [ad07b65]
|
|
1349
|
+
- Updated dependencies [41f498b]
|
|
1350
|
+
- Updated dependencies [ef0d150]
|
|
1351
|
+
- Updated dependencies [f34226e]
|
|
1352
|
+
- Updated dependencies [564b605]
|
|
1353
|
+
- Updated dependencies [e1d4251]
|
|
1354
|
+
- Updated dependencies [40d3a33]
|
|
1355
|
+
- Updated dependencies [1184192]
|
|
1356
|
+
- Updated dependencies [a2a9747]
|
|
1357
|
+
- Updated dependencies [a1609a6]
|
|
1358
|
+
- Updated dependencies [37f6844]
|
|
1359
|
+
- Updated dependencies [2b50261]
|
|
1360
|
+
- Updated dependencies [ac600e5]
|
|
1361
|
+
- Updated dependencies [d374caf]
|
|
1362
|
+
- Updated dependencies [c1ef923]
|
|
1363
|
+
- Updated dependencies [af5e292]
|
|
1364
|
+
- Updated dependencies [7f96b10]
|
|
1365
|
+
- Updated dependencies [167ec42]
|
|
1366
|
+
- Updated dependencies [0046d8f]
|
|
1367
|
+
- Updated dependencies [f1d4748]
|
|
1368
|
+
- Updated dependencies [bea374e]
|
|
1369
|
+
- Updated dependencies [b1119ec]
|
|
1370
|
+
- Updated dependencies [9f23d2b]
|
|
1371
|
+
- Updated dependencies [578e025]
|
|
1372
|
+
- Updated dependencies [af025ee]
|
|
1373
|
+
- Updated dependencies [d109a4d]
|
|
1374
|
+
- Updated dependencies [598c89a]
|
|
1375
|
+
- Updated dependencies [b8b9af4]
|
|
1376
|
+
- Updated dependencies [31676be]
|
|
1377
|
+
- Updated dependencies [8c0d52e]
|
|
1378
|
+
- Updated dependencies [70a774b]
|
|
1379
|
+
- Updated dependencies [9ce096f]
|
|
1380
|
+
- Updated dependencies [e05db88]
|
|
1381
|
+
- Updated dependencies [ad13d63]
|
|
1382
|
+
- Updated dependencies [5ffcc14]
|
|
1383
|
+
- Updated dependencies [d971e51]
|
|
1384
|
+
- Updated dependencies [97abb24]
|
|
1385
|
+
- Updated dependencies [deb157a]
|
|
1386
|
+
- Updated dependencies [9c60144]
|
|
1387
|
+
- Updated dependencies [d2ce342]
|
|
1388
|
+
- Updated dependencies [9695da7]
|
|
1389
|
+
- Updated dependencies [58b8346]
|
|
1390
|
+
- Updated dependencies [a9e17b4]
|
|
1391
|
+
- Updated dependencies [b8ce7dc]
|
|
1392
|
+
- Updated dependencies [dbbd38a]
|
|
1393
|
+
- Updated dependencies [8871c14]
|
|
1394
|
+
- Updated dependencies [dfc6975]
|
|
1395
|
+
- Updated dependencies [3cf4de0]
|
|
1396
|
+
- Updated dependencies [c9dc811]
|
|
1397
|
+
- Updated dependencies [144ef9b]
|
|
1398
|
+
- Updated dependencies [a0b9e91]
|
|
1399
|
+
- Updated dependencies [99bd015]
|
|
1400
|
+
- Updated dependencies [21e4585]
|
|
1401
|
+
- @object-ui/types@17.6.0
|
|
1402
|
+
- @object-ui/i18n@17.6.0
|
|
1403
|
+
- @object-ui/react@17.6.0
|
|
1404
|
+
- @object-ui/core@17.6.0
|
|
1405
|
+
- @object-ui/sdui-parser@17.6.0
|
|
1406
|
+
- @object-ui/react-runtime@17.6.0
|
|
1407
|
+
|
|
3
1408
|
## 17.5.0
|
|
4
1409
|
|
|
5
1410
|
### Minor Changes
|