@object-ui/components 17.3.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 +2732 -1
- package/README.md +38 -38
- 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 +102 -0
- package/dist/custom/index.d.ts +28 -27
- 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 +36 -6
- package/dist/index.d.ts +21 -20
- package/dist/index.js +4991 -4464
- package/dist/index.umd.cjs +10 -6
- package/dist/lib/form-control-dom-props.d.ts +77 -0
- package/dist/lib/sidebar-cookie.d.ts +55 -0
- package/dist/lib/slider-thumb.d.ts +86 -0
- package/dist/notifications/index.d.ts +5 -5
- package/dist/renderers/action/action-bar.d.ts +3 -3
- package/dist/renderers/action/action-button.d.ts +12 -2
- package/dist/renderers/action/action-group.d.ts +10 -4
- package/dist/renderers/action/action-icon.d.ts +10 -2
- package/dist/renderers/action/action-menu.d.ts +20 -2
- package/dist/renderers/action/auto-trigger.d.ts +20 -0
- package/dist/renderers/action/visibility-gate.d.ts +81 -0
- package/dist/renderers/basic/elements.d.ts +3 -3
- package/dist/renderers/basic/html-elements.d.ts +1 -1
- package/dist/renderers/basic/record-picker.d.ts +10 -1
- package/dist/renderers/complex/data-table.d.ts +100 -4
- package/dist/renderers/form/validationRuleKeys.d.ts +45 -0
- package/dist/renderers/layout/containers.d.ts +2 -1
- 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 +1 -1
- 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 +14 -14
- 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,2691 @@
|
|
|
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
|
+
|
|
1408
|
+
## 17.5.0
|
|
1409
|
+
|
|
1410
|
+
### Minor Changes
|
|
1411
|
+
|
|
1412
|
+
- dc2aa3e: The action renderers publish the modern `UIActionSchema`, and every `forwardRef` renderer's props parameter is annotated so its declared types survive
|
|
1413
|
+
|
|
1414
|
+
**Breaking semantics (declared `minor` per the repo's version-alignment rule — objectui#4403 precedent — never `major`).** Six exported declarations in `@object-ui/components` change the action type they name, from the `@deprecated` legacy `ActionSchema` (`crud.ts`) to `UIActionSchema` (`ui-action.ts`):
|
|
1415
|
+
|
|
1416
|
+
- `ActionBarSchema.actions`, `ActionBarSchema.systemActions`
|
|
1417
|
+
- `ActionMenuSchema.actions`
|
|
1418
|
+
- `ActionGroupSchema.actions`
|
|
1419
|
+
- `ActionButtonProps.schema`, `ActionIconProps.schema`
|
|
1420
|
+
|
|
1421
|
+
The two types are not interchangeable in either direction. `UIActionSchema` requires `name`, which legacy inherits as optional from `BaseSchema`; legacy pins `type: 'action'` where these renderers serve `'script' | 'url' | 'modal' | 'flow' | 'api'`; and only the modern type declares `locations`, `target`, `endpoint`, `bodyExtra`, `bodyShape` and a `variant` union containing `'primary'` — all of which the implementations already read. objectui#4417 measured four compiler errors proving the VALUES were modern while the DECLARATIONS said legacy; this moves the declarations to match, so the contract and the implementation finally agree.
|
|
1422
|
+
|
|
1423
|
+
No runtime behaviour changes, and no published surface is involved: none of the six declarations is re-exported from the package index, and the sweep found zero type-checked consumers outside each declaration's own file. Metadata that renders today renders identically — the renderers read the same keys through the same paths.
|
|
1424
|
+
|
|
1425
|
+
Separately, all fifteen `schema`-reading `forwardRef` renderers in the package now annotate their render function's first parameter directly, and carry the pass-through index signature on that annotation rather than on the `forwardRef` type argument. `forwardRef` routes its type argument through `PropsWithoutRef`, whose `Omit` collapses a props type carrying `[key: string]: any` down to the bare index signature — every declared property erased, silently, with `noImplicitAny` reporting clean because the `any` is supplied explicitly by the index signature. That is what hid the declaration/implementation drift above for as long as it lasted. Thirteen renderers recover a real declared type for `schema` (the two raw-tag factories keep `any`, which is what they genuinely declare), and a new structural guard, `forwardref-props-annotation.guard.test.ts`, fails on any future `forwardRef` that reintroduces either half of the trap.
|
|
1426
|
+
|
|
1427
|
+
- cb13400: One fullscreen long-text editor, hoisted to the package both render paths may import
|
|
1428
|
+
|
|
1429
|
+
The "expand to a full-height dialog" interaction had two independent implementations. `FullscreenTextarea` lived inside the form renderer's built-in (unregistered) `textarea` branch in `@object-ui/components`; `FullscreenFieldEditor` lived in `@object-ui/fields` and served the registered `TextAreaField` / `RichTextField` widgets. They exist because ONE form-level promise — `ObjectFormSchema.mobile.fullscreenLongText`, projected onto every long-text field as `mobile_fullscreen` — is honoured on two render paths, and each path grew its own answer.
|
|
1430
|
+
|
|
1431
|
+
Two copies of a state machine drift, and these did, in both directions: objectui#3400 measured a read-only long-text field that was fully editable through the built-in branch's dialog (and "Done" wrote the edit into form state), objectui#3402 measured the same write-back hole for `disabled` on the registered path, and objectui#3393 (the dialog title needs the field label) and objectui#3272 (the copy needs i18n) each landed on one side before the other. Every repair was correct and none of them scaled.
|
|
1432
|
+
|
|
1433
|
+
`@object-ui/components` now exports `FullscreenEditor`, a single primitive owning the affordance, the dialog, the draft/commit state machine and the copy. The direction follows the measured import graph rather than fighting it: `@object-ui/fields` depends on `@object-ui/components`, and `components` declares no dependency on `fields` in either `dependencies` or `peerDependencies`, so the shared code can only live in `components`. `FullscreenFieldEditor` becomes a thin wrapper over it and keeps its name, its props and its test-id namespaces, so both hosts and their pins are unchanged.
|
|
1434
|
+
|
|
1435
|
+
The load-bearing part of the merge is that the primitive DEFINES `readOnly` and `disabled` instead of inheriting them by accident. Neither copy defined both: the built-in one grew them under objectui#3400, while the fields one declared only `disabled` and was shielded from `readonly` by its hosts' early return — a single implementation cannot be shielded by one caller's control flow. So both are answered once, and both call paths inherit the same answers: `readOnly` renders no affordance at all (it means "shown plainly", so advertising an expand button the user cannot use is worse than showing none), `disabled` leaves an inert one (it means "not interactive, muted"). Neither relies on the toggle alone, because `disabled` also carries the form's `isSubmitting` and can flip to true while the dialog is already open — so opening refuses independently of the attribute, the injected editor is told, "Done" is disabled, and `onCommit` is gated as the single point where a value leaves for host state.
|
|
1436
|
+
|
|
1437
|
+
No copy changed and no locale pack needed an edit: the primitive consumes the same `form.fullscreen.*` / `common.cancel` keys both copies already read, through `createSafeTranslation` with English defaults byte-identical to the literals, so provider-less hosts render exactly what they did. The now-unread `form.fullscreen.*` defaults are dropped from `useFieldTranslation`, where they would have re-created in the defaults map precisely the duplication this change removes from the components.
|
|
1438
|
+
|
|
1439
|
+
`toggleClassName` is not carried into the new primitive. It was declared on `FullscreenFieldEditorProps` and written by nobody — zero producers repo-wide — and `FullscreenFieldEditor` is not exported from the `@object-ui/fields` barrel, so no consumer outside the package could ever have set it. Minting it as part of a NEW public export in `@object-ui/components` would have published a prop with no producer, the shape objectui#3232/#3233 keeps deleting.
|
|
1440
|
+
|
|
1441
|
+
- 38ab505: Retire the `global_nav` Studio designer surfaces, and track the `@objectstack` family at `17.0.0-rc.6` (objectstack#7100 / objectstack#6888).
|
|
1442
|
+
|
|
1443
|
+
## The retirement
|
|
1444
|
+
|
|
1445
|
+
`global_nav` was an `ACTION_LOCATIONS` member no running-app surface ever rendered. The console's ⌘K palette (`app-shell/src/chrome/CommandPalette.tsx`) builds its groups from nav items, objects, dashboards, pages, reports, recent items, record search and theme; it holds no reference to `global_nav`, to `actionRendersAt`, or to any action-metadata source. An action declaring `locations: ['global_nav']` therefore never reached a user.
|
|
1446
|
+
|
|
1447
|
+
The Studio designer previewed it anyway — a mock frame reading `⌘K · Command palette` with the author's button inside it. That is the sharp edge the maintainer's 2026-08-09 ruling on objectstack#6888 named: an authoring tool promising a surface the product does not have teaches authors, and every AI copying this corpus, to declare dead metadata. `@objectstack/spec` `17.0.0-rc.6` retired the member (7 members → 6) with a named rejection message; this release removes the designer surfaces that outlived it.
|
|
1448
|
+
|
|
1449
|
+
- `metadata-admin/previews/ActionPreview.tsx` — the mock command-palette placement frame is gone. The metadata strip above it still ECHOES whatever `locations` the draft declares, deliberately: reporting what a (possibly stale) draft says is honest, whereas the frame CLAIMED the platform renders it.
|
|
1450
|
+
- `metadata-admin/inspectors/ActionDefaultInspector.tsx` — the `global_nav` entry is gone from `LOCATION_LABELS`. That map is typed `Record< ActionLocation, string >`, so the retirement reached it as a compile error rather than as a silently stale dropdown — the mechanism objectui#3017 installed, firing as designed.
|
|
1451
|
+
- `metadata-admin/previews/block-config.ts` — the `record:quick_actions` location dropdown no longer offers it, and both locale tables drop the now-orphaned `…option.location.global_nav` key.
|
|
1452
|
+
- `@object-ui/components`' `action:bar` doc comment is aligned. The component's published enum is `[...ACTION_LOCATIONS]`, so it followed the retirement on its own; only the prose was stale.
|
|
1453
|
+
|
|
1454
|
+
`@object-ui/core`'s `ActionEngine.getActionsForLocation` is **unchanged and still answers a literal string match**. Narrowing it to the six live members would put a second rejection point beside the schema's — the tolerant-consumer shape the strict-contract rule forbids, inverted. Enforcement stays where it belongs: the parameter type is now six-membered so no type-correct caller can spell the retired value, and `ActionLocationSchema` rejects it by name at authoring and publish time.
|
|
1455
|
+
|
|
1456
|
+
## The dependency move
|
|
1457
|
+
|
|
1458
|
+
All 37 `@objectstack/*` declarations across 30 `package.json` files move from `^17.0.0-rc.5` to `^17.0.0-rc.6`, and `pnpm-lock.yaml` resolves one copy of each family package at rc.6. The siblings move with `spec` because `client` / `formula` / `lint` pin it **exactly** — leaving them behind would keep two copies of the spec in the tree, the split brain objectui#3560 called out.
|
|
1459
|
+
|
|
1460
|
+
Bumping the pin and repairing the fallout cannot be split: at rc.5 the `Record< ActionLocation, string >` above is missing a key, at rc.6 it has an excess one.
|
|
1461
|
+
|
|
1462
|
+
## Breaking, in FROM → TO form
|
|
1463
|
+
|
|
1464
|
+
- **`@object-ui/types`' `Theme` now binds the spec's `Theme`, not `ThemeInput`.** rc.6 retired every `…Input` alias and moved the bare name onto the `z.input` side (`X` = `z.input`, `XParsed` = `z.infer`). The runtime shape and this package's exported name are unchanged — `Theme` was, and still is, the AUTHORING shape where `mode` is optional. Re-pointing at `ThemeParsed` would have been the silent swap.
|
|
1465
|
+
- **`SpecReport` / `SpecReportChart` re-point to `ReportParsed` / `ReportChartParsed`, and `SpecReportInput` / `SpecReportChartInput` to `Report` / `ReportChart`.** Same rename, same rule: each local alias keeps the SIDE it had at rc.5.
|
|
1466
|
+
- **`@object-ui/types` no longer re-exports `I18nObject`, `LocaleConfig`, `PluralRule`, `DateFormat` or `NumberFormat`** — all five were retired by rc.6. They were dead re-exports here: nothing in this repo imported them from `@object-ui/types` (`@object-ui/i18n`'s formatter vocabulary in `utils/spec-formatters.ts` is locally declared and never bound the spec symbols). `I18nLabel` survives and is unchanged as a name.
|
|
1467
|
+
- **`I18nLabel` itself widened from `string` to `string | Record< string, string >`** — rc.6 folded the retired `I18nObject`'s per-locale map into it and ships `resolveI18nLabel(label, locale)` as the shared resolver. Every read in this repo that lands in a text slot now goes through that resolver, so an inline map renders its locale instead of `[object Object]`. Reads the compiler cannot see are audited separately in objectui#4163.
|
|
1468
|
+
- **`@object-ui/types`' `GlobalFilterSchema` derives via `.safeExtend`, not `.extend`.** rc.6's `GlobalFilterSchema` carries a refinement and zod 4 refuses `.extend()` on a refined object outright, which threw at module load. `.safeExtend` is zod's prescribed replacement and KEEPS the refinement, so the spec's cross-field rule now also runs on this package's dialect — which is the intended behaviour, since the pinned divergences widen individual fields and were never meant to switch off a whole-object rule.
|
|
1469
|
+
|
|
1470
|
+
- c1d939f: One `SchemaNode`, and one label vocabulary — the union wins, and labels resolve where the locale lives
|
|
1471
|
+
|
|
1472
|
+
Two packages published a type called `SchemaNode` and they were not the same type. `@object-ui/core` hand-declared `interface SchemaNode { type: string; … [key: string]: any }`; `@object-ui/types` exported `type SchemaNode = BaseSchema | string | number | boolean | null | undefined`, whose own doc comment names `'Plain string'` a valid node. Both were exported under one name from packages the same consumers import together, so which declaration a call site got depended on which package it happened to import from — #4548's canary measured 19 of 35 errors as exactly that collision. Core's declaration is now a re-export of types', so there is one declaration left to disagree with. Core's entry surface is unchanged: `dist/index.d.ts` is byte-identical across the change.
|
|
1473
|
+
|
|
1474
|
+
Reconciling it exposed a real defect rather than a mechanical narrowing, which is why the first attempt was withdrawn instead of forced. The spec bridges write `spec.label` — the spec's `I18nLabel`, an INLINE locale map like `{ en: 'Owner', 'zh-CN': '负责人' }` — into `node.label`, and `BaseSchema.label` declared `string`. Under core's old index signature that assignment was invisibly `any`; under one honest `SchemaNode` it is a type error. `BaseSchema.label` and `.description` therefore now accept `string | I18nLabel`, and the two bridge assignments compile with their expressions untouched.
|
|
1475
|
+
|
|
1476
|
+
Resolution happens at READ time, in the renderer, against the display locale — not at the bridge. Resolving at the bridge was measured unimplementable: it is a plain class method that cannot call a hook, `BridgeContext` declares no locale, and `updateContext()` has zero callers, so a bridge-resolved label would freeze one audience's language into the node tree with no re-translation channel. React's own invalidation re-translates for free at the read site.
|
|
1477
|
+
|
|
1478
|
+
The widening turned every blind `schema.label`-as-string read into a named compiler error, and that inventory is the audit: it named four sites repo-wide, all one class — the label reaching a React child position, where a map does not render as `[object Object]` but THROWS `Objects are not valid as a React child`, failing the whole subtree. Three are `@object-ui/components` renderers (`filter-builder`, `sidebar-group`, `dropdown-menu`), which now resolve with the spec's own `resolveI18nLabel` against `useDisplayLocale()`. The fourth is `plugin-dashboard`'s `DashboardGridLayout` heading, which resolves with `pickLocalized` against the active UI language — matching the widget-title resolution already in that same component rather than putting two resolvers and two disagreeing locale channels in one render; the two resolvers are limb-for-limb twins with a parity test pinning them.
|
|
1479
|
+
|
|
1480
|
+
One interface now carries both label vocabularies two properties apart — `label`/`description` are the spec's INLINE map, `ariaLabel` is the KEYED bundle reference — and each accepts the other's shape vacuously. That confusability is objectui#4167's known hazard, inherent to the spec's `I18nLabel` design; both shapes are named with cross-referenced doc comments stating which resolver owns which slot, and a pin asserts the two unions do not collapse into each other.
|
|
1481
|
+
|
|
1482
|
+
Finally, the spec bridges declare their return type as `BaseSchema` instead of the union. Both bridges end in a single `return node` on an object literal, so the union described nothing real while forcing a narrowing at every read — 272 mechanical errors across five suites in the first round. That change is a type annotation only; the emitted JavaScript is byte-identical.
|
|
1483
|
+
|
|
1484
|
+
- bb68488: `element:record_picker` publishes `sort`, `limit` and `emptyText` as authoring
|
|
1485
|
+
inputs (objectui#4167).
|
|
1486
|
+
|
|
1487
|
+
All three were already READ by the renderer and declared by the contract — the
|
|
1488
|
+
renderer has passed `sort` into `$orderby` and `limit` into `$top` since the
|
|
1489
|
+
block existed, and `emptyText` decides the no-rows message — but none of them
|
|
1490
|
+
appeared in `inputs`, so every layer that reads a manifest said they did not
|
|
1491
|
+
exist. `packages/components/src/renderers/layout/page.tsx` builds the JSX-page
|
|
1492
|
+
compiler's prop whitelist from `getKnownTypes()` plus these `inputs`, so writing
|
|
1493
|
+
any of the three on a JSX page drew an `unknown-prop` warning from
|
|
1494
|
+
`sdui-parser/src/validate.ts` on a key the renderer then went on to honour.
|
|
1495
|
+
|
|
1496
|
+
That is objectui#3407's shape — honoured, undiscoverable — and this is the same
|
|
1497
|
+
repair objectui#3808 made for `record:details.hideFields` and objectui#3830 made
|
|
1498
|
+
for `element:record_picker.filter`. `@objectstack/spec` 17.0.0-rc.6 is what made
|
|
1499
|
+
it actionable: objectstack#5775 declared the three upstream, and the reverse
|
|
1500
|
+
direction of the console's registry parity gate went red demanding them the
|
|
1501
|
+
moment the pin moved — a red the previous exemption had predicted in writing and
|
|
1502
|
+
called "correct and wanted".
|
|
1503
|
+
|
|
1504
|
+
Each description documents the renderer's real behaviour rather than restating
|
|
1505
|
+
the schema, because that is the half an author cannot read off the contract:
|
|
1506
|
+
|
|
1507
|
+
- **`sort`** and **`limit`** are both overridden OUTRIGHT by a node-level
|
|
1508
|
+
`dataSource` binding (`dataSource.sort ?? sort`), not merged with it — so a
|
|
1509
|
+
node that carries a `dataSource` silently ignores them.
|
|
1510
|
+
- **`limit`** defaults to 50 in the renderer, not in the schema, and a record
|
|
1511
|
+
outside the limit cannot be picked at all with nothing in the control to say
|
|
1512
|
+
more exist.
|
|
1513
|
+
- **`emptyText`** is published as `string` against a contract of
|
|
1514
|
+
`string | Record< string, string >`: rc.6 widened it to `I18nLabel`, and this
|
|
1515
|
+
renderer passes the value straight into a text node with no locale resolution,
|
|
1516
|
+
so only the plain-string form renders today. The description says so rather
|
|
1517
|
+
than advertising a shape the renderer drops — the narrowed-type treatment
|
|
1518
|
+
objectui#3832 describes, with the render-site gap tracked in objectui#4163.
|
|
1519
|
+
|
|
1520
|
+
The console's `registry-inputs-spec-parity` suite also drops all twelve of its
|
|
1521
|
+
off-spec exemptions, which rc.6 obsoleted at once (objectstack#6776 declared
|
|
1522
|
+
`page:header.recordChrome` / `showStar` / `showCopyId`, `page:accordion.variant`
|
|
1523
|
+
and `page:tabs.tabStyle`; objectstack#5775 declared the `element:record_picker`
|
|
1524
|
+
trio and `children` on the four page containers). The forward direction of that
|
|
1525
|
+
gate now runs with no cover of any kind.
|
|
1526
|
+
|
|
1527
|
+
- bb68488: Stop declaring 14 symbols under names `@objectstack/spec` owns at `17.0.0-rc.6`
|
|
1528
|
+
(objectui#4167, objectstack#4115).
|
|
1529
|
+
|
|
1530
|
+
The rc.6 bump published nine names this repo already declared locally, on top of
|
|
1531
|
+
four that predate it — `check:spec-symbols` reported all thirteen at once, and a
|
|
1532
|
+
fourteenth (`GlobalFilterSchema`) appeared during the bump itself. Each was
|
|
1533
|
+
triaged on its own rather than blanket-renamed, because the right answer differs
|
|
1534
|
+
per symbol: five bind to the spec, three are renamed because the spec's
|
|
1535
|
+
same-named export means something else, five arrive by derivation, and one is a
|
|
1536
|
+
declared dialect with a written reason.
|
|
1537
|
+
|
|
1538
|
+
**Breaking for importers of `@object-ui/react`, `@object-ui/app-shell` and
|
|
1539
|
+
`@object-ui/types`** — three exported names changed, because the spec exports the
|
|
1540
|
+
same name for a _different_ thing:
|
|
1541
|
+
|
|
1542
|
+
| package | was | now | what the spec's same-named export actually is |
|
|
1543
|
+
| :-------------------- | :----------------- | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1544
|
+
| `react` / `app-shell` | `MetadataState` | `MetadataCacheState` | a metadata item's LIFECYCLE state — `'draft' \| 'active' \| 'deprecated' \| 'archived'` (`MetadataStateSchema`, `@objectstack/spec/system`) |
|
|
1545
|
+
| `react` / `app-shell` | `resolveI18nLabel` | `resolveKeyedI18nLabel` | a resolver for the INLINE per-locale map (`{ en: 'Owner', 'zh-CN': '负责人' }`) against a BCP-47 locale |
|
|
1546
|
+
| `types` | `DateRangePreset` | `FilterBuilderDateRangePreset` | the thirteen HISTORICAL dashboard filter-bar presets; this one is the filter-builder set, which adds eight FUTURE windows the dashboard schema rejects |
|
|
1547
|
+
|
|
1548
|
+
`resolveI18nLabel` is the one where the collision had already started costing
|
|
1549
|
+
something. rc.6 widened `I18nLabel` from `string` to
|
|
1550
|
+
`string | Record< string, string >`, so the same authored value now reaches
|
|
1551
|
+
either resolver — and each answers wrongly, silently, for the other's input: the
|
|
1552
|
+
keyed one returns `undefined` for `{ en: 'Owner' }` (no `key`, no
|
|
1553
|
+
`defaultValue`), and the spec's reads `key` / `defaultValue` / `params` as locale
|
|
1554
|
+
tags. The rc.6 bump PR met this and aliased the spec's import as
|
|
1555
|
+
`resolveInlineI18nLabel` in five files, with hand-written comments at two of
|
|
1556
|
+
them. That is a review convention, which is what objectstack#4115 exists to
|
|
1557
|
+
replace with a rule — so `Keyed` is now the counterpart of that `Inline`, and the
|
|
1558
|
+
name says which vocabulary it resolves at every call site.
|
|
1559
|
+
|
|
1560
|
+
**Eleven keep their names and are now imported or derived from the spec** instead
|
|
1561
|
+
of re-declared: `DATE_RANGE_PRESETS`, `NavigationMode`, `AddressValue`,
|
|
1562
|
+
`BreakpointColumnMap`, `BreakpointOrderMap`, `KanbanConfig`, `CalendarConfig`,
|
|
1563
|
+
`GanttConfig`, plus the three renamed above at their new names.
|
|
1564
|
+
|
|
1565
|
+
**Four of the copies were losing information, not just duplicating it.**
|
|
1566
|
+
|
|
1567
|
+
- **`GanttConfig` declared six keys and called itself canonical; rc.6's
|
|
1568
|
+
`GanttConfigSchema` declares seventeen.** The eleven it never mentioned —
|
|
1569
|
+
`parentField`, `typeField`, `baselineStartField`, `baselineEndField`,
|
|
1570
|
+
`groupByField`, `resourceView`, `assigneeField`, `effortField`, `capacity`,
|
|
1571
|
+
`quickFilters`, `autoZoomToFilter` — are all read by
|
|
1572
|
+
`plugin-gantt/src/ObjectGantt.tsx`, through a local `GanttConfigEx`
|
|
1573
|
+
intersection that existed only because this type did not carry them. It now
|
|
1574
|
+
derives from the spec, with `timeSegments` (shift segmentation) as the one
|
|
1575
|
+
genuinely local extension; the schema is `$loose` upstream, so that key is
|
|
1576
|
+
legal metadata rather than a second dialect.
|
|
1577
|
+
- **`GanttConfig.tooltipFields` carried the comment "not part of the upstream
|
|
1578
|
+
GanttConfigSchema".** It is, as of rc.6, so the key now arrives from the spec.
|
|
1579
|
+
- **`AddressValue` declared five of the spec's seven parts** — `countryCode` and
|
|
1580
|
+
`formatted` were missing, under a comment already claiming to be "the part
|
|
1581
|
+
names of `AddressSchema`". The widget still renders five inputs; binding the
|
|
1582
|
+
type stops it from asserting the platform cannot store the other two, and makes
|
|
1583
|
+
the `{ ...address }` write-through say so.
|
|
1584
|
+
- **`DATE_RANGE_PRESETS` was `Object.keys(PRESET_RANGES)`,** a third copy of a
|
|
1585
|
+
vocabulary the spec extracted in objectstack#4614 precisely to collapse — its
|
|
1586
|
+
own doc comment names this module as one of the three. It is now the spec's
|
|
1587
|
+
array by reference, and the local date-macro bounds table is pinned complete
|
|
1588
|
+
against it with `satisfies`, so a preset the schema gains without bounds here
|
|
1589
|
+
is a compile error rather than a filter that validates clean and then selects
|
|
1590
|
+
nothing.
|
|
1591
|
+
|
|
1592
|
+
`NavigationMode` was one hop from the spec already (`NavigationConfig['mode']`);
|
|
1593
|
+
it is bound directly, with a both-directions type pin that it stays the same type
|
|
1594
|
+
as the config's own `mode`. `KanbanConfig` / `CalendarConfig` /
|
|
1595
|
+
`BreakpointColumnMap` / `BreakpointOrderMap` were exact hand copies of `$strict`
|
|
1596
|
+
schemas and are now re-exports — "still exact" is the argument for binding them,
|
|
1597
|
+
since a copy with nothing to protect can only drift.
|
|
1598
|
+
|
|
1599
|
+
`GlobalFilterSchema` is the one ALLOW entry. It is the same spread-composition
|
|
1600
|
+
dialect as `SelectOptionSchema` next to it, and it collided only because rc.6's
|
|
1601
|
+
new refinement forced `.extend()` to be respelled as a `.shape` spread — which
|
|
1602
|
+
moved a derivation the guard could see into an object literal it deliberately
|
|
1603
|
+
does not descend into. The dialect is unchanged and its three divergences are
|
|
1604
|
+
pinned; which side moves on the refinement itself is objectui#4165.
|
|
1605
|
+
|
|
1606
|
+
`@objectstack/spec` moves from `devDependencies` to `dependencies` in
|
|
1607
|
+
`@object-ui/layout`: its public type surface now references the spec.
|
|
1608
|
+
|
|
1609
|
+
### Patch Changes
|
|
1610
|
+
|
|
1611
|
+
- ceccdcf: Action confirm dialogs and success toasts now honour the bundle's translated
|
|
1612
|
+
`confirmText` / `successMessage`, not just `label` (objectui#4265).
|
|
1613
|
+
|
|
1614
|
+
A TranslationBundle entry for an action carries three keys under one
|
|
1615
|
+
`_actions.<name>` node — `label`, `confirmText`, `successMessage` — and
|
|
1616
|
+
`useObjectLabel()` has always exposed a resolver for each. What had drifted was
|
|
1617
|
+
the call sites: `page:header` (authored record pages), `record:quick_actions`
|
|
1618
|
+
and the related-list row menu resolved the button `label` only and dispatched
|
|
1619
|
+
the authored `confirmText` / `successMessage` untouched. One bundle entry met
|
|
1620
|
+
two fates: the button rendered the translation, the confirm dialog rendered the
|
|
1621
|
+
authored English.
|
|
1622
|
+
|
|
1623
|
+
All action-rendering surfaces now go through one resolver,
|
|
1624
|
+
`useActionTextLocalizer()` (new, exported from `@object-ui/react`), which
|
|
1625
|
+
applies the existing `actionLabel` / `actionConfirm` / `actionSuccess`
|
|
1626
|
+
resolvers over the three keys together. Fallback is unchanged: with no bundle
|
|
1627
|
+
entry — or an entry lacking a key — the authored text renders. A bundle cannot
|
|
1628
|
+
introduce a `confirmText` or `successMessage` the metadata never declared.
|
|
1629
|
+
|
|
1630
|
+
- d6e5124: An action rendered in the overflow menu, as an icon or inside a group now reaches the runner carrying the same authored keys as the same action rendered inline — `action:menu`, `action:icon` and `action:group` forward `label` and `description`, and the two group/icon surfaces also forward `resultDialog`.
|
|
1631
|
+
|
|
1632
|
+
Every action renderer hands the `ActionRunner` an explicit key WHITELIST rather than the action itself. That is deliberate — a key no renderer honours must not look wired — but the whitelists had drifted, and which renderer a given action gets is decided by `action:bar`'s `maxVisible` split (3 on desktop, 1 on mobile) and by `systemActions`, which are always in the overflow menu. So the same declared action behaved differently depending on the viewport.
|
|
1633
|
+
|
|
1634
|
+
`label` and `description` are what the console's param-collection handler titles its dialog from (`title: action?.label || action?.title`, `description: actionDescription(…, action?.description)`). Dropped, an action with declared `params` opened a dialog titled "Action parameters" while the SAME declaration rendered inline named itself "Create Environment". `resultDialog` is the one-shot reveal spec (a fresh 2FA code, a newly minted OAuth secret): dropped, the runner falls back to the success toast and the value the user was meant to copy is gone — the objectui#3646 defect, still live on two of the four declared surfaces.
|
|
1635
|
+
|
|
1636
|
+
`undoable` and `recordIdField` are deliberately NOT added. Both are read only under a `rowRecord` guard, and `rowRecord` is `params._rowRecord`, written exclusively by the spread-based hosts (`DeclaredActionsBar`, `RelatedRecordActionsBridge`, `ObjectGrid`, `page:header`), none of which dispatch through these renderers. They are unreachable on this path rather than dropped — `action:button` forwards them here inertly — so forwarding them would have added a second inert copy instead of restoring an affordance.
|
|
1637
|
+
|
|
1638
|
+
A new repo gate, `pnpm check:action-forward-parity`, now derives each surface's owed key set (`authorable ∩ runtime-read − retired`) from the spec's own schemas and the consumers' ASTs and fails when a renderer drops one, so the seventh instance of this class fails on the pull request that introduces it rather than shipping green.
|
|
1639
|
+
|
|
1640
|
+
- debad27: An `autoTrigger` action that spills past `action:bar`'s `maxVisible` now still runs — `action:menu` consumes the flag instead of dropping it.
|
|
1641
|
+
|
|
1642
|
+
`autoTrigger` is the client-composed "run this action as soon as a renderer receives it" flag behind deep links like the welcome page's "Create your environment" CTA (#844). It was consumed only by `action:button`. `action:bar` splits its post-gate list at `maxVisible` (3 on desktop, 1 on mobile) and hands the tail to `action:menu`, which had no `autoTrigger` handling at all — so an auto-triggered action that happened to sort past that threshold was rendered as an ordinary "More" menu entry and never ran, while the caller had already spent the one-shot signal it stood for. The `?runAction=create_environment` deep link is consumed by stripping it from the URL, so the measured end state was `urlParam=null execute=0`: no dialog, and no URL left to retry from. Which actions lost their auto-trigger was partly a function of viewport width, since `maxVisible` drops to 1 on mobile, and `systemActions` — always in the overflow menu, whatever the viewport — could never fire one at all.
|
|
1643
|
+
|
|
1644
|
+
The flag's contract is now stated and enforced as "execute once on mount by whichever renderer receives the action". `action:menu` consumes it by EXECUTING, through the same path a click on that item takes; it does not open the dropdown, so a transport flag never moves what the user sees. Consumption happens where the action provably arrives — the menu renderer receiving it — not in the menu items, which Radix mounts only once the dropdown opens and which would therefore have waited on the very click the flag exists to avoid.
|
|
1645
|
+
|
|
1646
|
+
Once-ness has one implementation (`renderers/action/auto-trigger.ts`), now shared by both renderers rather than written twice: a guard ref per rendered action, so re-renders never re-fire it and a flag that flips true later still fires exactly once. Container visibility still governs mounting — a hidden `action:bar` or `action:menu` renders no children and auto-triggers nothing — while the action's own `visible` gate does not suppress the trigger, matching `action:button`'s long-standing behaviour so that a deep link cannot depend on where the bar happened to put the action.
|
|
1647
|
+
|
|
1648
|
+
The `action:bar` split, the inline `action:button` path and #4166's arming pins are unchanged.
|
|
1649
|
+
|
|
1650
|
+
- f650253: `BaseSchema.ariaLabel` declares the keyed i18n vocabulary the renderer actually
|
|
1651
|
+
resolves, `.disabled` accepts the predicate string it actually evaluates, and the
|
|
1652
|
+
keyed shape finally has a name (objectui#4581)
|
|
1653
|
+
|
|
1654
|
+
Three slots on one base type had drifted from what the renderer does with them.
|
|
1655
|
+
PR #4593 fixed `visible` and measured the rest; these are the rest.
|
|
1656
|
+
|
|
1657
|
+
`ariaLabel` was `string`, but `SchemaRenderer.tsx:111` resolves it with
|
|
1658
|
+
`resolveKeyedI18nLabel`, whose input is the KEYED form
|
|
1659
|
+
`{ key, defaultValue?, params? }` — a reference into a translation bundle. It is
|
|
1660
|
+
now `string | KeyedI18nLabel`, and `KeyedI18nLabel` is a new exported type in
|
|
1661
|
+
`@object-ui/types` rather than a fourth inline copy of one object literal: the
|
|
1662
|
+
three that existed (`@object-ui/react`'s resolver, `@object-ui/layout`'s
|
|
1663
|
+
`resolveLabel`, `@object-ui/app-shell`'s `t`-taking twin) were verified identical
|
|
1664
|
+
in their object half first, and two of them now import the name.
|
|
1665
|
+
|
|
1666
|
+
The vocabulary matters more than the widening. `#4581` originally asked for
|
|
1667
|
+
`string | I18nLabel`, and that spelling was withdrawn as measured-wrong: the
|
|
1668
|
+
spec's `I18nLabel` is the INLINE LOCALE MAP (`string | Record<string, string>`),
|
|
1669
|
+
a different vocabulary resolved against a BCP-47 locale by a different function
|
|
1670
|
+
of a confusingly similar name. Under it the shipped keyed fixture type-checked
|
|
1671
|
+
only vacuously — as a locale map whose "locales" are named `key` and
|
|
1672
|
+
`defaultValue` — the same label carrying `params` was rejected outright, and a
|
|
1673
|
+
genuine `{ en: 'Owner' }` compiled while rendering an EMPTY `aria-label`. Naming
|
|
1674
|
+
the keyed shape is the declaration half of the fix objectui#4167 started on the
|
|
1675
|
+
naming side; `@object-ui/app-shell`'s copy keeps its inline spelling for now
|
|
1676
|
+
because an open PR has a pending change to that file, and the comment there says
|
|
1677
|
+
so.
|
|
1678
|
+
|
|
1679
|
+
`disabled` was `boolean` on a key the renderer never reads as one:
|
|
1680
|
+
`SchemaRenderer.tsx:466` evaluates it through the same `evaluateCondition` as
|
|
1681
|
+
`visible`, and a `disabledOn?: string` sibling exists for the same reason. It is
|
|
1682
|
+
now `boolean | string`. The asymmetry with `visible` was accidental rather than
|
|
1683
|
+
deliberate.
|
|
1684
|
+
|
|
1685
|
+
Both are widenings on authored-input-dominant properties: authors gain a
|
|
1686
|
+
spelling, nothing that type-checked before stops doing so, and readers already
|
|
1687
|
+
coped with `any` through `BaseSchema`'s index signature. Three test fixtures that
|
|
1688
|
+
had been casting past these declarations with `as unknown as BaseSchema` state
|
|
1689
|
+
their values directly now, and the declared unions are pinned invariantly so
|
|
1690
|
+
neither a missing widening nor an overshoot to `any` can pass unnoticed.
|
|
1691
|
+
|
|
1692
|
+
Declaring the vocabulary honestly also surfaced a real one: the `toggle`
|
|
1693
|
+
renderer writes `aria-label` itself instead of going through SchemaRenderer's
|
|
1694
|
+
resolver, and it forwarded the raw value. Invoked directly it emitted
|
|
1695
|
+
`aria-label="[object Object]"` for a keyed label — announced verbatim by a
|
|
1696
|
+
screen reader. It resolves now. Through `SchemaRenderer` the bug was invisible,
|
|
1697
|
+
because SchemaRenderer injects its own resolved `aria-label` afterwards; a
|
|
1698
|
+
downstream type-check sweep found it, not a test.
|
|
1699
|
+
|
|
1700
|
+
`BaseSchema.label` and `.description` are deliberately unchanged and pinned that
|
|
1701
|
+
way. They receive the spec's inline `I18nLabel` from the view bridges, which is a
|
|
1702
|
+
real defect, but resolving it belongs at the spec-to-schema boundary rather than
|
|
1703
|
+
in this declaration — and that work is still blocked on a design question about
|
|
1704
|
+
where the display locale enters, so it is not in this release.
|
|
1705
|
+
|
|
1706
|
+
- d0c3b26: Every plain `<button>` now declares its `type`. HTML defaults an untyped button to
|
|
1707
|
+
`type="submit"`, so any of these buttons would submit the form it was composed into
|
|
1708
|
+
instead of running its own handler — a real risk for renderers (`drawer`, `tree-view`,
|
|
1709
|
+
`navigation-overlay`) whose placement inside a form is a JSON metadata decision. 114
|
|
1710
|
+
sites were converted to `type="button"`; no site was a genuine submit button, and the
|
|
1711
|
+
DOM is otherwise unchanged.
|
|
1712
|
+
|
|
1713
|
+
The defect class is now closed mechanically by a new `object-ui/button-has-type` ESLint
|
|
1714
|
+
rule (error), so the next untyped button fails CI at write time rather than being found
|
|
1715
|
+
by a fourth audit round (objectui#4045, closing the objectui#3344 family).
|
|
1716
|
+
|
|
1717
|
+
- 4dadf0d: `@object-ui/components` compiles under `noImplicitAny` — the workspace's last strict-relaxing package
|
|
1718
|
+
|
|
1719
|
+
`packages/components/tsconfig.json` carried `"noImplicitAny": false`, the only place in the workspace that relaxed a `strict` sub-flag, under a comment that explained the neighbouring `rootDir` removal rather than the flag itself. `tsconfig.test.json` mirrored the one flag deliberately, so that a test project could not become the compiler of record for a source strictness decision the build config owns. Both now simply inherit `strict: true` from the root config, and the mirror's reasoning is rewritten to record why the mirror is gone rather than deleted silently.
|
|
1720
|
+
|
|
1721
|
+
Turning the flag on reported 26 implicitly-`any` sites in five renderer source files and 2 in the package's own tests, all of which now have real types. Nothing about the runtime changed; every one of the package's 1077 tests passes untouched.
|
|
1722
|
+
|
|
1723
|
+
Two of those signatures were typed by measurement rather than by preference, and both are worth recording:
|
|
1724
|
+
|
|
1725
|
+
The ten `sidebar.tsx` entry points follow the convention the package's other registered renderers already use — an inline `{ schema: <X>Schema; [key: string]: any }` annotation naming the registered component's own schema type (21 occurrences across the renderer tree, against zero uses of `ComponentRendererProps`). Only `'sidebar'` itself has a schema type in the registry map; the other ten registrations are sidebar _parts_ with none of their own, so they take `BaseSchema`, the type every registered node satisfies. Annotating them `SidebarSchema` would have asserted `type: 'sidebar'` on a node whose type is `'sidebar-header'`.
|
|
1726
|
+
|
|
1727
|
+
The action renderers' callbacks are typed from `UIActionSchema`, not the legacy `ActionSchema` those three files import for their declarations. The legacy interface (`crud.ts`, already `@deprecated`) has no `locations`, so the shared `actionRendersAt` placement predicate rejects it outright; its `variant` union has no `'primary'`, the value the objectui#2339 ordering tie-break compares against; and its `type` is the literal `'action'`, while the actions actually flowing through these renderers carry `'form' | 'script' | 'url' | 'flow' | 'api' | 'modal'`. `action:bar`'s own documented example is a `UIActionSchema`. None of this was checkable before, because the props type never reached the callbacks at all: `forwardRef` routes props through `PropsWithoutRef`, whose `Omit` collapses a props type carrying `[key: string]: any` down to the bare index signature, so `schema` arrived as `any` and every callback under it inferred `any` too. The fix annotates each action list once where it enters and lets the `filter`/`some`/`map` chains below infer.
|
|
1728
|
+
|
|
1729
|
+
Graded `patch`: no declaration this package publishes changes shape. The three action schema interfaces and the leaf components whose props moved to `UIActionSchema` are internal — none is re-exported from `src/index.ts`. The `actions?: ActionSchema[]` keys those interfaces still declare remain on the legacy type; reconciling that declaration with the type the implementation actually receives reaches roughly 46 sites across 12 files and is filed separately.
|
|
1730
|
+
|
|
1731
|
+
- ae10a01: Console chrome reaches the bundle — the list switcher, the aggregate footer, the dialog a11y fallbacks and the whole Settings namespace screen stop being English on non-English consoles
|
|
1732
|
+
|
|
1733
|
+
Six strings on the two screens a user looks at most were hardcoded English literals rather than bundle lookups, so they stayed English on every non-English console with nothing an app could author to change them. They are not object, field, view or action labels — no key in `TranslationData` reaches them — while the console's own bundle already ships zh-CN, ja-JP, es-ES, de, fr, pt, ru, ko and ar and translates hundreds of neighbouring strings. Omissions from an otherwise complete bundle, not a missing capability.
|
|
1734
|
+
|
|
1735
|
+
**Two of the six needed no new keys at all, which is the more interesting half.** The list-view mode switcher named its nine visualizations from a private `VIEW_LABELS` table while `console.objectView.viewType*` — the same nine words — had been resolved through the bundle by the create-view picker for months; the switcher now reads those keys, so the picker's 「画廊」 and the switcher's 「画廊」 cannot drift apart in nine languages. The create/edit dialog's close button is the remainder of a fix that already landed: objectstack#5505 routed the `sr-only` close label through `common.close` for the two Shadcn-synced primitives, but `MobileDialogContent` is a hand-written wrapper outside that regeneration zone with its own close button, and it is exactly what `ModalForm` renders — so the dialog the report measured was the one place still announcing "Close" in English.
|
|
1736
|
+
|
|
1737
|
+
The aggregate footer is the one the original report singled out: the **number** was already locale-formatted and the **prefix** was a hardcoded `Avg: ` / `Sum: `. All eleven aggregation kinds now take their prefix from `grid.summary.*`, and the label/value join is its own key rather than a `': '` baked into the renderer — the separator is translatable content, so zh sets a fullwidth colon and fr the French space-before-colon. The numbers are untouched. The form dialog's `sr-only` description fallback joins the packs too; it is clipped, not visible, so the only way an app could displace it was to author a `description` and thereby put a visible subtitle on every dialog.
|
|
1738
|
+
|
|
1739
|
+
**The Settings namespace screen converts as one unit.** `SettingsView` routed zero framing copy through i18n — save/failure toasts, the env-lock and crypto refusals, the load-error card, the empty-route state, the navigation buttons, the unsaved-changes save bar — while its immediate sibling `SettingsHub`, in the same directory, resolved everything through `t('console.settingsHub.*')`. A zh-CN admin read correctly translated field labels sitting inside an English save bar, because `useSettingsLabel` translates a namespace's authored content but reaches none of the chrome around it. All of it now resolves through a `console.settingsView.*` namespace placed beside the hub's, including the crypto-refusal strings that objectui#4579 deliberately left in English rather than leave one translated string among a dozen literals.
|
|
1740
|
+
|
|
1741
|
+
The save-bar counter was an English plural rule executing in every locale (`change` plus an `s` when the count exceeds one). It is now a real i18next plural family — base key plus `_one` and `_other` in all ten packs — not the `(s)` spelling translated nine ways. The base key is the load-bearing part: i18next asks `Intl.PluralRules` for the one suffix a language needs and, finding no such slot, falls back to English, so without it Russian would read English at counts 2-20 and Arabic at 2-99. Russian and Arabic take the "noun: {count}" form their packs already use for this exact reason, and the counter is verified rendering in-language at 1, 2 and 5.
|
|
1742
|
+
|
|
1743
|
+
The Beta badge reuses the hub's existing key rather than minting a twin, and the refusal messages interpolate their subject through the bundle instead of concatenating a translated word onto an English prefix.
|
|
1744
|
+
|
|
1745
|
+
- 4b70d28: data-table row menu — the built-in Edit/Delete predicate parameters are derived from the authoring type, not hand-restated
|
|
1746
|
+
|
|
1747
|
+
One authoring shape (`DataTableSchema.rowEditPredicates` / `rowDeletePredicates`, objectui#2614) had grown four separate declarations in `renderers/complex/data-table.tsx`: the shared `isBuiltinRowActionVisible` gate and `planDataTableRowMenu` each hand-wrote `{ visibleWhen?: unknown }`, and the row-menu ITEM component hand-wrote the full `{ visibleWhen?: unknown; disabledWhen?: unknown }` pair. Nothing tied any of them to the type whose values they receive, so a rename in `@object-ui/types` would have left all four compiling against a shape that no longer existed — the objectui#3009 hand-copy family, in miniature.
|
|
1748
|
+
|
|
1749
|
+
Each one now derives. The planner keeps its deliberate visibility-only subset, `Pick`ed from the very key its caller passes (`Pick<NonNullable<DataTableSchema['rowEditPredicates']>, 'visibleWhen'>` and the delete twin), so the signature still tells the truth about what the function reads while becoming structurally unable to drift from what it subsets. The two consumers that serve both built-ins share one derived alias taken from the union of the twins, so they may only read keys both schema keys declare. Measured: with `visibleWhen` renamed in `@object-ui/types`, the previous hand-written declarations still type-check clean, the derived ones fail to compile.
|
|
1750
|
+
|
|
1751
|
+
No behavior change — no runtime code was touched, and the package's suite passes unchanged. Alongside it, the "a disabled item still counts toward the menu" rule gains the pin it never had where a user meets it: a row whose only action is `disabledWhen`-gated keeps its "⋮" trigger, and that trigger opens the item, present and `aria-disabled`. The two halves of that rule live in different functions, and each half's own test stayed green while the other regressed.
|
|
1752
|
+
|
|
1753
|
+
- b4d3c22: `element:button`'s action forward is excess-property checked again — a misspelled key on that payload is now a compile error, not silence
|
|
1754
|
+
|
|
1755
|
+
The payload `element:button` hands to `execute(…)` closed with `as any`. An assertion asks only for comparability, so it switched TypeScript's excess-property (freshness) check off for the whole literal: all sixteen keys the renderer forwards rode unchecked, and `ActionDef` being a closed type (objectui#4046) bought this surface nothing. A typo added to that list — `refreshAftr`, `confirmTxt` — would have compiled, published, and reached a runner that silently does nothing, which is the objectstack#2169 "Mark Done does nothing" shape the closed type exists to prevent.
|
|
1756
|
+
|
|
1757
|
+
The exemption that recorded this assumed the cast was load-bearing, on the reasoning that `element:button` receives a bare `action?: Record< string, any >` prop rather than a typed action, so removing the cast would be a contract change. Measured on TypeScript 6.0.3, that was wrong on the point that mattered: dropping the cast type-checks clean as-is, because every key the literal writes is already declared on `ActionDef`. The prop's type is a separate question and is deliberately untouched here — the forward literal never needed the cast to compile.
|
|
1758
|
+
|
|
1759
|
+
Two literals needed it, not one. The `execute(…)` argument is contextually typed by `execute(action: ActionDef)`, so dropping the cast is enough for the explicit keys. The `paramsPayload` binding spread into it is the second, easily-missed half: a spread source's own keys are not checked _through_ the spread, so `{ actionParams }` / `{ params }` could still invent a key while the payload around them was checked. That binding is now annotated `ActionDef` too.
|
|
1760
|
+
|
|
1761
|
+
Runtime behaviour is unchanged — the object reaching `execute` is identical, key order included, and the emitted `.d.ts` does not move. What changes is that the compiler now rejects an invented key here, verified in both directions: the same probe key produces `TS2353` after this change and no diagnostic at all before it.
|
|
1762
|
+
|
|
1763
|
+
With this surface fixed, `check:action-forward-parity` has no payloads exempt from its freshness rule: all five action-forwarding renderers write into a literal the compiler checks, and the gate's ratchet removed the exemption entry itself as designed.
|
|
1764
|
+
|
|
1765
|
+
- bc64bfe: A dependency-gated option list no longer deletes the field's stored value on mount
|
|
1766
|
+
|
|
1767
|
+
The four fixed-option widgets (`SelectField`, `MultiSelectField`, `CheckboxesField`, `RadioField`) end their cascade resolution with a "drop what is no longer offered" effect, and the form renderer runs an equivalent clear of its own over every option field. Both read `resolveCascadingOptions`, which returns an **empty** offered set whenever the list is _gated_ — a declared `dependsOn` parent is still empty. Nothing the field held could be "still offered" against an empty set, so both paths wrote the field empty **on mount, with no interaction**, while the control rendered "Select Country first" beside it: it told the user it could not offer anything, and deleted what they had.
|
|
1768
|
+
|
|
1769
|
+
Gated means **unknown**, not invalid. The cascade clear exists (ADR-0058) so a user-driven parent change prunes a now-invalid child; a withheld list on mount is missing information — the record simply arrived with its controlling field empty (a later-cleared parent, an import, a partially-migrated row) — and that is not a reason to destroy stored data. Both clears now skip while gated, reading the resolver's own `gated` flag rather than re-deriving it from an empty offered set, which would collide with the distinct never-configured case guarded separately in objectui#4220.
|
|
1770
|
+
|
|
1771
|
+
Convergence stays exactly where it belongs: once the parent **is** chosen and the resolved set genuinely excludes the stored value, the prune applies unchanged — including at the moment the gate lifts, so picking a parent whose list does not contain the old value still clears it on that transition. The three states are pinned apart (never-configured / gated / resolved-and-excludes) across all four widgets and the form host, so a future edit cannot collapse them back into one empty-set test.
|
|
1772
|
+
|
|
1773
|
+
Reachable on every host that mounts these widgets with a live record: the form renderer, the grid's inline cell editor, and the detail page's inline editor — where each `onChange` went straight into the record draft the save bar commits.
|
|
1774
|
+
|
|
1775
|
+
- 3e19fe7: i18n copy: one ellipsis glyph across the ten packs, `usted` in the es draft-preview empty state, and a pt sentence that stops contracting `de` onto its own hole
|
|
1776
|
+
|
|
1777
|
+
Three locale-copy defects that no gate could see, because all three are _value_ defects on keys whose names, placeholders and key sets were already correct.
|
|
1778
|
+
|
|
1779
|
+
**One ellipsis (objectui#3878).** `en` ended 33 values with three ASCII full stops (`Loading...`, `Ask anything...`) and 110 with the typographic ellipsis `…`, and the nine translation packs had copied `en` value by value — so a user could read both glyphs on one screen: `common.loading` beside `dashboard.loading`, `console.ai.askAnything` beside its own panel's siblings. All ten packs now spell it `…` (U+2026), per the maintainer-authorized consistency pass registered on objectstack#6015. 312 pack values changed: 34 in `en` (the 33 trailing plus the one mid-sentence `collaboration.commentPlaceholder`) and 278 across the nine. Eleven inline `defaultValue` call sites were re-synchronised with the new `en` text, which `scripts/check-i18n-call-site-keys.mjs` requires byte-for-byte.
|
|
1780
|
+
|
|
1781
|
+
The convention is now pinned so the split cannot regrow: `packages/i18n/src/__tests__/ellipsis-glyph-3878.test.ts` fails, by key name, on any value in any of the ten packs that holds three ASCII full stops. It is deliberately wider than "a trailing `...` in `en`", because the census showed the narrow rule would have shipped with two holes in it — `collaboration.commentPlaceholder` puts the ellipsis mid-sentence, and `list.loading` had the packs wrong while `en` was already right, which no `en`-only rule can see.
|
|
1782
|
+
|
|
1783
|
+
Fifteen module-local **no-provider fallback** entries were moved with the packs, across `useCollaborationTranslation`, `useFieldTranslation`, `useDetailTranslation`, `ObjectGrid`, `KanbanImpl`, `data-table` and `ConnectionStatus`. Those maps exist to render when no `LocalizationProvider` is mounted, and each one's own docblock requires it to stay byte-identical to the `en` pack — a requirement objectui#3440 already enforces mechanically for the collaboration map. Leaving them behind would have made the provider-less path disagree with the provider path on ten keys.
|
|
1784
|
+
|
|
1785
|
+
**es `usted` (objectui#3875).** `preview.empty.notReadyDescription` said `Revisa la conversación` — the tú imperative — in a namespace that is otherwise 23:1 usted, and it renders _underneath the usted draft-preview banner at the same moment_, not before or after it. `Revisa` → `Revise`; nothing else in the sentence carries a register. The neighbouring `approvalsInbox` namespace is legitimately tú and was left alone.
|
|
1786
|
+
|
|
1787
|
+
**pt contraction (objectui#3877).** `ConcurrentUpdateDialog` splits `detail.concurrentUpdateDescription` on `{{field}}` and renders a bolded label in the gap, and pt left a bare `de` in front of that gap. When the multi-field conflict branch passes the record label (`este registro`), Portuguese users read `de este registro` — a contraction error every native speaker sees, and one that no spelling of the leaf value could fix (`deste registro` renders `de deste registro`). The pt sentence is rewritten so the hole is preceded by the verb `afeta` instead of any preposition, which closes the whole class rather than trading `de` for an `em` or `a` that contract just as hard. pt only; `en` is unchanged.
|
|
1788
|
+
|
|
1789
|
+
No behavior, no keys added or removed, no placeholder changed.
|
|
1790
|
+
|
|
1791
|
+
- 45e1949: Numbers render in the user's locale, and a `Field.number` year is no longer `2,026`
|
|
1792
|
+
|
|
1793
|
+
Every numeric field the console rendered went through an `Intl.NumberFormat` built with the locale hardcoded to `en-US` and `useGrouping` never set. Two defects rode in that one construction: a `zh-CN` or `de-DE` console still grouped and pointed decimals the US way, and a four-digit **year** stored as `Field.number({ scale: 0 })` rendered as `2,026` — in every locale, with no field property able to turn it off. Apps had been converting year columns to `Field.text` to escape it, permanently trading numeric comparison, range filters and dataset dimension types for a display detail.
|
|
1794
|
+
|
|
1795
|
+
The construction had been copied into five places — the number cell renderer, the currency cell renderer, the `CurrencyField` widget, the compact `formatNumber` helper, and the dashboard `MetricWidget` — so fixing any one surface never changed the answer. They now share one formatter, `formatDisplayNumber` in `@object-ui/i18n`, which owns the locale and the grouping policy together, plus one locale resolver, `useDisplayLocale`.
|
|
1796
|
+
|
|
1797
|
+
`useDisplayLocale` composes the two locale channels this repo already had rather than adding a third: the tenant's regional default (`useLocalization().locale`, ADR-0053) when an org has configured one, otherwise the active UI language (`useObjectTranslation().language`) so grouping and decimal marks follow a language switch. That second step is what covers the case the report was measured in — a fresh database, where the tenant localization endpoint has no locale to give.
|
|
1798
|
+
|
|
1799
|
+
Grouping is now suppressed when a field declares `scale: 0` and carries no currency, which is what makes years, fiscal periods and other ordinals render plainly. This is an **interim default** with an accepted cost: a large scale-0 _count_ loses its separators too. It holds only until the spec gains an authorable presentation hint, which is being specified separately, contract-first; when that lands it overrides this heuristic.
|
|
1800
|
+
|
|
1801
|
+
Three surfaces deliberately keep their separators, because a zero-decimal display there does not come from a field declaration: the dashboard `MetricWidget` (its decimals are parsed from a numeral.js format pattern, and its own contract calls the separators load-bearing — "`1,930,000` not `1930000`"), the `element:number` aggregate renderer, and every currency path including amounts whose currency code could not be resolved. An **undeclared** `scale` also keeps grouping — absent means "decimals unknown", not "integer".
|
|
1802
|
+
|
|
1803
|
+
`formatCurrency`, `formatCompactCurrency` and `formatNumber` each take a new optional trailing `locale` argument. Existing calls are unaffected; omitting it now follows the runtime default rather than forcing US conventions.
|
|
1804
|
+
|
|
1805
|
+
- a3ae404: fix(components,plugin-dashboard): a static-data `table` widget renders instead of crashing
|
|
1806
|
+
|
|
1807
|
+
A dashboard widget authored as `{ type: 'table', options: { data: [ … ] } }` fell into the
|
|
1808
|
+
error boundary with "Maximum update depth exceeded" the moment its tile re-rendered, while
|
|
1809
|
+
every chart family on the identical static surface rendered clean.
|
|
1810
|
+
|
|
1811
|
+
- `data-table` no longer re-renders itself to death. Its `columns` / `data` fallbacks are
|
|
1812
|
+
module-scope empties instead of per-render array literals, and the prop→state column sync
|
|
1813
|
+
re-seeds on a value change rather than on a new identity — so a consumer that derives its
|
|
1814
|
+
columns each render (which both dashboard surfaces do) costs the table nothing.
|
|
1815
|
+
- Both dashboard surfaces now give the static table the `columns` key `DataTableSchema`
|
|
1816
|
+
requires, derived from the rows when the author declared none — the same derivation the
|
|
1817
|
+
`provider: 'object'` half of the widget family already performed. Previously such a table
|
|
1818
|
+
drew one empty row per record: no headers, no cells.
|
|
1819
|
+
- `DashboardGridLayout` reads an authored `options.data` ARRAY for its static table, which
|
|
1820
|
+
its `widgetData?.items` expression resolved to `[]`. `DashboardRenderer` had the arm all
|
|
1821
|
+
along.
|
|
1822
|
+
|
|
1823
|
+
- bfdf3d4: `element:record_picker.filter` is now discoverable from the published `inputs`
|
|
1824
|
+
|
|
1825
|
+
The fourth A-class gap of objectui#3808's own list, and the one its three-way
|
|
1826
|
+
triage dropped: `filter` appears in that issue's raw key dump for this block and
|
|
1827
|
+
then in none of its A / B / C lists, so the change that added the repo-wide
|
|
1828
|
+
parity gate exempted it by name instead of declaring it. It is the same shape as
|
|
1829
|
+
the four #3808 fixed — `@objectstack/spec` declares
|
|
1830
|
+
`ElementRecordPickerProps.filter`, the renderer has read it all along
|
|
1831
|
+
(`composed?.filter ?? props.filter`, straight into the picker query's `$filter`),
|
|
1832
|
+
and the registry `inputs` never mentioned it.
|
|
1833
|
+
|
|
1834
|
+
`element:record_picker` is not in the public tier ("record picking is a field
|
|
1835
|
+
widget, not a page block"), so the gap was not in `sdui.manifest.json` — it was
|
|
1836
|
+
in the JSX-page compiler's prop whitelist, which `renderers/layout/page.tsx`
|
|
1837
|
+
builds from `getKnownTypes()` plus these same `inputs`. A JSX page writing
|
|
1838
|
+
`filter` therefore got an `unknown-prop` warning from `sdui-parser`'s prop walk
|
|
1839
|
+
on the very key that decided which records the picker offered, and the designer
|
|
1840
|
+
panel gave an author no way to discover the key existed at all.
|
|
1841
|
+
|
|
1842
|
+
The description is derived from what the renderer does, not from restating the
|
|
1843
|
+
spec's one-liner, because the one thing an author cannot read off the spec is
|
|
1844
|
+
which of the two places they may write a filter wins: a node-level `dataSource`
|
|
1845
|
+
filter (itself AND-combined with any saved `view` it names) is taken and this
|
|
1846
|
+
top-level `filter` is DROPPED, not merged — so this key applies only when the
|
|
1847
|
+
node carries no `dataSource` filter.
|
|
1848
|
+
|
|
1849
|
+
`type` is `'object'`, taken from the spec's actual shape on the resolved pin
|
|
1850
|
+
rather than the `'array'` the issue's landing sketch guessed:
|
|
1851
|
+
`FilterConditionSchema` is `z.record(z.string(), z.unknown())` intersected with
|
|
1852
|
+
the `$and` / `$or` / `$not` group, so a rule array is rejected. This is the one
|
|
1853
|
+
key in the family where `ComponentInput`'s coarse typing costs nothing —
|
|
1854
|
+
`sdui-parser`'s `checkType` accepts exactly the values the spec accepts here, so
|
|
1855
|
+
unlike `element:text_input.defaultValue` there is no narrowing to disclose.
|
|
1856
|
+
|
|
1857
|
+
The parity gate's explicit exemption for this key is deleted in the same change
|
|
1858
|
+
(its own `carries no stale unpublished-key exemption` assertion demands it), and
|
|
1859
|
+
the key joins #3808's four in the by-name "declared, not merely not-failing" pin.
|
|
1860
|
+
|
|
1861
|
+
- b1e42d0: Conditional required (`requiredWhen`) now decides at SUBMIT time too — the star and the validator can no longer disagree
|
|
1862
|
+
|
|
1863
|
+
A `requiredWhen` predicate that flipped to FALSE after the dialog mounted updated only half the form. The display layer re-evaluated correctly — the asterisk and `aria-required` both disappeared — while submit stayed refused with "<field> is required" and no write was ever issued. The user saw an optional field and a form that would not save, with nothing on screen naming the field it was still waiting on (objectui#4161).
|
|
1864
|
+
|
|
1865
|
+
The cause is not a mount-time snapshot, which is what the symptom looks like. The renderer hands react-hook-form its per-field rules as a `<Controller rules>` prop, and RHF _merges_ that object into the field descriptor it already holds — `_f: { ...previous._f, ...options }`. A rule key that stops being spelled is therefore never removed. Rules could be ADDED live (a predicate flipping TRUE after mount did start enforcing, correctly) but never withdrawn: the `validate.required` entry installed the first time the predicate evaluated TRUE outlived every later FALSE verdict. The validation layer was append-only, latched on the first TRUE the field ever produced.
|
|
1866
|
+
|
|
1867
|
+
The `validate.required` entry is now registered unconditionally and decides required-ness when it _runs_, reading the live verdict the renderer publishes on every render — the same single `resolveFieldRuleState` result that draws the asterisk, not a second evaluation of the predicate with its own copy of the record assembly. Both directions are pinned: a predicate flipping FALSE re-opens submit, a predicate flipping TRUE starts enforcing, and statically required fields are unaffected.
|
|
1868
|
+
|
|
1869
|
+
- 3f5f87c: `SchemaRenderer` states its real contract — a typed, required `schema` and a deliberate forwarding surface
|
|
1870
|
+
|
|
1871
|
+
`SchemaRenderer` is the renderer loop: every registered SDUI component is rendered through it. It handed `forwardRef` a props type of `{ schema: SchemaNode } & Record<string, any>`, which puts `string` into `keyof Props`, so `'ref' extends keyof Props` was always true, React's `PropsWithoutRef` took its `Omit` branch, and `Omit` over a type carrying a string index signature keeps only the index signature. Every declared prop was erased. Measured on the pre-fix source: `keyof ComponentProps<typeof SchemaRenderer>` was `string` and `ComponentProps<typeof SchemaRenderer>['schema']` was `any`, while the type argument went on declaring `SchemaNode`. The other half is the same defect seen from the call site — `<SchemaRenderer />` with no schema at all, `<SchemaRenderer schema={12345} />`, and an arbitrary misspelled prop each type-checked in silence. This is objectui#4422 / PR #4438's trap in the most central component in the repo, spelled `Record<string, any>` rather than `[key: string]: any`, which is why every previous sweep's grep and both shipped guards' detector reported the site as clean.
|
|
1872
|
+
|
|
1873
|
+
Graded **minor, not major**, on objectui#4528's reasoning: the type argument has always DECLARED `schema`; the index signature erased it from the resolved type, and restoring what the declaration documents is a fix to the published contract rather than a contract break.
|
|
1874
|
+
|
|
1875
|
+
**The forwarding surface is kept, deliberately.** This component forwards every prop it does not read to the component the schema names, resolved at runtime from a plugin-extensible registry — `packages/react/README.md` documents exactly that, and `@object-ui/components`' form renderer consumes the `onSubmit` it shows being forwarded. Closing that surface would state a false contract and would force every leaf plugin's props into this package. So the two halves are separated: the `forwardRef` type argument is the honest `SchemaRendererProps`, with no index signature for `PropsWithoutRef` to collapse, and the open surface is stated once in an explicit export annotation, which nothing routes through `Omit`. The published `.d.ts` shows the erasure disappearing: `ForwardRefExoticComponent<Omit<{ schema: SchemaNode } & Record<string, any>, "ref"> & RefAttributes<any>>` becomes `ForwardRefExoticComponent<SchemaRendererProps & Record<string, any> & RefAttributes<any>>`.
|
|
1876
|
+
|
|
1877
|
+
`SchemaRendererProps.schema` is declared as `BaseSchema | string | null | undefined` — what this component actually handles. It previously declared `@object-ui/core`'s `SchemaNode` interface, which requires `type: string` and so contradicted the component's own early returns for strings and nullish, while every caller held `@object-ui/types`' wider union. The erasure hid that mismatch completely.
|
|
1878
|
+
|
|
1879
|
+
**One declared behaviour change.** A non-object, non-string primitive schema now renders as its own text. It previously fell through to the shallow copy `{ ...schema }`, which spreads a primitive to an empty object, lost the `type` the renderer then looked up, and surfaced the red "Unknown component type: undefined" box — an accident of the spread rather than a decision. The declared props type excludes `number` / `boolean` so no author is invited to pass them; the runtime handling is defence-in-depth for untyped callers and stored metadata. Strings, `null`, `undefined`, `0` and `false` render exactly as before, and an object naming an unregistered type still gets the error box; all four are pinned.
|
|
1880
|
+
|
|
1881
|
+
Latent defects the erasure had been hiding, each surfaced by the repo-wide type-check and fixed at its call site: `DashboardRenderer` cast its widget schema to `Record<string, any>`, dropping the `type` every branch of `getComponentSchema` sets; `DashboardGridLayout`'s equivalent now states its return type instead of inferring a union that admitted a shape with no `type`; and `ReportViewer` handed a section's `content` array to the renderer whole, so a multi-node section rendered the unknown-component box instead of its content — arrays are mapped rather than widened into the renderer's declared input.
|
|
1882
|
+
|
|
1883
|
+
A repo-wide structural guard replaces the two per-package siblings' blocked direction: it judges every `forwardRef` in `packages/*/src` (219 sites) and its detector resolves `Record<string, …>` and `string`-keyed mapped types in addition to literal index signatures — the spelling the previous detector went blind on. It judges the type argument only, where an index signature is an accidental eraser, and never an export annotation, where one is a stated contract.
|
|
1884
|
+
|
|
1885
|
+
- f5e1143: A collapsed sidebar now survives a reload — `SidebarProvider` reads the `sidebar_state` cookie it has always written
|
|
1886
|
+
|
|
1887
|
+
The cookie half of this feature only ever ran in one direction. `setOpen` wrote `sidebar_state` on every toggle with a 7-day max-age, and nothing ever read it back: `SidebarProvider` seeded its state from `defaultOpen` (default `true`), so a sidebar you collapsed came back expanded on the next load with the correct cookie sitting right there, unread. QA measured it at 255px and `data-state=expanded` at +2s, +4s and +8s after load, reproduced three times.
|
|
1888
|
+
|
|
1889
|
+
Upstream Shadcn closes this loop in a **server component** — it reads the cookie there and passes the value down as `defaultOpen`. A pure SPA like the console has no such step, which is why nothing downstream could paper over it: passing a cookie-derived `defaultOpen` from one shell would have fixed that shell and left every other consumer of the primitive broken. The read therefore happens client-side, in the provider, as a lazy `useState` initialiser rather than a mount effect — the state has to be right on the first render, since a post-mount correction would still flash an expanded sidebar at the user.
|
|
1890
|
+
|
|
1891
|
+
Precedence is now pinned, in this order: a controlled `open` prop, then the cookie, then `defaultOpen`, then `true`. The cookie overrides the _default_, never a controlled usage. With no cookie present the behaviour is exactly what it was before, which is what keeps explicit `defaultOpen={false}` call sites — the marketing demos in `apps/site` — rendering unchanged; those cases are controls in the new test file and are green on both sides of the change.
|
|
1892
|
+
|
|
1893
|
+
Only the two values the writer produces are honoured (`"true"` / `"false"`), matched on an exact cookie name; anything else, including an absent or malformed value, falls through to `defaultOpen` rather than inventing a preference the user never expressed. The reader is SSR-safe, which `apps/site` needs: those primitives are `"use client"`, and Next still renders them on the server for the initial HTML, where there is no `document`.
|
|
1894
|
+
|
|
1895
|
+
Because `packages/components/src/ui/**` is regenerated from the Shadcn registry, the primitive itself only gains two anchored one-liners. All of the parsing lives in `packages/components/src/lib/sidebar-cookie.ts`, which the sync never touches, and the two edits are declared in `scripts/shadcn-local-patches.mjs` so `pnpm shadcn:update` re-applies them instead of silently reverting the fix — the same mechanism already used for the translated `Sheet`/`Dialog` close labels.
|
|
1896
|
+
|
|
1897
|
+
- 5bf09fd: `ActionParamDialog`'s `select` branch no longer renders a hardcoded English `Select...` placeholder. The fallback used when an action param declares no `placeholder` of its own now reads the existing `common.select` pack key, so it is translated in all ten locales and carries the typographic ellipsis (U+2026) that #3878 converged the packs on. Authored `placeholder` metadata keeps priority, and no locale pack changed — the key was reused from `LookupField`'s identical select-trigger use.
|
|
1898
|
+
- Updated dependencies [0e67b53]
|
|
1899
|
+
- Updated dependencies [ceccdcf]
|
|
1900
|
+
- Updated dependencies [ee66e2e]
|
|
1901
|
+
- Updated dependencies [ee26e65]
|
|
1902
|
+
- Updated dependencies [5900ac5]
|
|
1903
|
+
- Updated dependencies [932cbcd]
|
|
1904
|
+
- Updated dependencies [734d186]
|
|
1905
|
+
- Updated dependencies [f650253]
|
|
1906
|
+
- Updated dependencies [3d9769a]
|
|
1907
|
+
- Updated dependencies [8f85f8b]
|
|
1908
|
+
- Updated dependencies [d0c3b26]
|
|
1909
|
+
- Updated dependencies [3fc2971]
|
|
1910
|
+
- Updated dependencies [aca27fa]
|
|
1911
|
+
- Updated dependencies [dde7283]
|
|
1912
|
+
- Updated dependencies [f7c6430]
|
|
1913
|
+
- Updated dependencies [ae10a01]
|
|
1914
|
+
- Updated dependencies [92876f0]
|
|
1915
|
+
- Updated dependencies [f279deb]
|
|
1916
|
+
- Updated dependencies [eb7f586]
|
|
1917
|
+
- Updated dependencies [e901131]
|
|
1918
|
+
- Updated dependencies [d9d3463]
|
|
1919
|
+
- Updated dependencies [2a40f69]
|
|
1920
|
+
- Updated dependencies [bec3e14]
|
|
1921
|
+
- Updated dependencies [613b167]
|
|
1922
|
+
- Updated dependencies [1f9b905]
|
|
1923
|
+
- Updated dependencies [828549a]
|
|
1924
|
+
- Updated dependencies [e1ade8f]
|
|
1925
|
+
- Updated dependencies [abb0f81]
|
|
1926
|
+
- Updated dependencies [38ab505]
|
|
1927
|
+
- Updated dependencies [3e19fe7]
|
|
1928
|
+
- Updated dependencies [bb58d1d]
|
|
1929
|
+
- Updated dependencies [5cc847c]
|
|
1930
|
+
- Updated dependencies [fa21254]
|
|
1931
|
+
- Updated dependencies [33c32bf]
|
|
1932
|
+
- Updated dependencies [66fb4fa]
|
|
1933
|
+
- Updated dependencies [b953a97]
|
|
1934
|
+
- Updated dependencies [d7f3e30]
|
|
1935
|
+
- Updated dependencies [6d641c9]
|
|
1936
|
+
- Updated dependencies [7e4f0e5]
|
|
1937
|
+
- Updated dependencies [a84385b]
|
|
1938
|
+
- Updated dependencies [45e1949]
|
|
1939
|
+
- Updated dependencies [92250d6]
|
|
1940
|
+
- Updated dependencies [c1d939f]
|
|
1941
|
+
- Updated dependencies [58bebf6]
|
|
1942
|
+
- Updated dependencies [405e808]
|
|
1943
|
+
- Updated dependencies [49ae9f4]
|
|
1944
|
+
- Updated dependencies [c0f9a4b]
|
|
1945
|
+
- Updated dependencies [2459a3e]
|
|
1946
|
+
- Updated dependencies [ac853ce]
|
|
1947
|
+
- Updated dependencies [fa51109]
|
|
1948
|
+
- Updated dependencies [d6aa172]
|
|
1949
|
+
- Updated dependencies [fe52a04]
|
|
1950
|
+
- Updated dependencies [d46f9b8]
|
|
1951
|
+
- Updated dependencies [3f5f87c]
|
|
1952
|
+
- Updated dependencies [2fea4d2]
|
|
1953
|
+
- Updated dependencies [7f1cb33]
|
|
1954
|
+
- Updated dependencies [f148a64]
|
|
1955
|
+
- Updated dependencies [bb68488]
|
|
1956
|
+
- Updated dependencies [2e3b0c0]
|
|
1957
|
+
- Updated dependencies [9461dd3]
|
|
1958
|
+
- Updated dependencies [78fa331]
|
|
1959
|
+
- Updated dependencies [47f551b]
|
|
1960
|
+
- Updated dependencies [31ab1ac]
|
|
1961
|
+
- Updated dependencies [0082db8]
|
|
1962
|
+
- Updated dependencies [ab04728]
|
|
1963
|
+
- Updated dependencies [06915b0]
|
|
1964
|
+
- Updated dependencies [ff84b05]
|
|
1965
|
+
- @object-ui/i18n@17.5.0
|
|
1966
|
+
- @object-ui/react@17.5.0
|
|
1967
|
+
- @object-ui/core@17.5.0
|
|
1968
|
+
- @object-ui/types@17.5.0
|
|
1969
|
+
- @object-ui/sdui-parser@17.5.0
|
|
1970
|
+
- @object-ui/react-runtime@17.5.0
|
|
1971
|
+
|
|
1972
|
+
## 17.4.0
|
|
1973
|
+
|
|
1974
|
+
### Minor Changes
|
|
1975
|
+
|
|
1976
|
+
- ecae400: Retire the `capability-multiselect` field widget name, which existed only on the docs-site registration path and which nothing ever stamped (objectui#3308, ADR-0049 enforce-or-remove)
|
|
1977
|
+
|
|
1978
|
+
`field:capability-multiselect` was registered by `registerFields()` and only there. That function's sole caller is the docs site, so the key never existed on the live path (`registerAllFields()`, run at module import, iterates `fieldWidgetMap` — which never listed it). A field authored with `widget: 'capability-multiselect'` therefore resolved to nothing in every real application, while the comment above the registration described it as usable from a record form: a code comment promising a capability that does not exist, which is the worst direction for a metadata renderer AI-authored apps read as authority.
|
|
1979
|
+
|
|
1980
|
+
Nothing stamped the hint either. ADR-0056 P1 stamps `permission-facet-link` on all six `sys_permission_set` facets — `system_permissions` included — through the single `ObjectStackAdapter.getObjectSchema` choke point, and P2 put the capability editor in Studio. The widget name was a leftover from an intermediate iteration of that rollout.
|
|
1981
|
+
|
|
1982
|
+
Removed, with a tombstone at each site:
|
|
1983
|
+
|
|
1984
|
+
- `@object-ui/fields` — the `field:capability-multiselect` registration and the comment that advertised it. **Breaking in name only**: the key was unreachable outside the docs site, so no application could have resolved it. A field still carrying the hint now degrades to its declared `type` renderer, the defined behavior for an unregistered widget.
|
|
1985
|
+
- `@object-ui/plugin-detail` — `InlineFieldInput`'s `widget === 'capability-multiselect'` branch, the hint's last honoring surface. Leaving one consumer for a name no producer emits and no form resolves is the same declared-vs-enforced split, inverted. The sibling `permission-facet-link` branch is untouched and pinned.
|
|
1986
|
+
- `@object-ui/components` — the dead `capability-multiselect` entry in the form renderer's `DATA_SOURCE_FIELD_TYPES` set, which could never match a resolvable widget.
|
|
1987
|
+
- `@object-ui/plugin-form` — a comment naming `capability-multiselect` as the widget stamped onto `sys_permission_set.system_permissions`; it names `permission-facet-link` now, which is what is actually stamped.
|
|
1988
|
+
|
|
1989
|
+
`CapabilityMultiSelectField` itself is **unchanged and still exported**: Studio's `PermissionMatrixEditor` imports and renders it directly, which is ADR-0056 P2's design. Only the widget name is retired — the component is not a registry field widget and its doc comment now says so.
|
|
1990
|
+
|
|
1991
|
+
`registerFields()` is also **kept**, with its `@deprecated Use registerAllFields() instead` note corrected. The two are not interchangeable: it registers `createFieldRenderer(widget)`, which synthesizes the label, description and the local `value`/`onChange` state that lets a bare field node (`{ type: 'currency', label: 'Amount' }`) render standalone in the docs demos. Retiring it needs a decision about where that demo chrome goes; the note now records that instead of implying a drop-in replacement.
|
|
1992
|
+
|
|
1993
|
+
### Patch Changes
|
|
1994
|
+
|
|
1995
|
+
- 794c497: `action:bar` member actions declaring `visible: false` are now hidden instead of rendered
|
|
1996
|
+
|
|
1997
|
+
`action:button` and `action:icon` carried the same truthiness gate objectui#3812
|
|
1998
|
+
removed from the member-action leaves — `if (schema.visible && !isVisible)
|
|
1999
|
+
return null` — so `visible: false`, the most explicit way an author can say
|
|
2000
|
+
"never show this", fell into the "no gate declared" branch and the action
|
|
2001
|
+
rendered anyway.
|
|
2002
|
+
|
|
2003
|
+
objectui#3812's triage judged the five component-level `schema.visible` gates a
|
|
2004
|
+
dormant defensive layer, because `packages/react`'s `SchemaRenderer` evaluates
|
|
2005
|
+
`newSchema.visible !== undefined` and hides the node before the component ever
|
|
2006
|
+
mounts. Two of the five are not dormant, and this is the difference:
|
|
2007
|
+
|
|
2008
|
+
`action:bar` does not route through `SchemaRenderer`. It resolves each member's
|
|
2009
|
+
renderer from the `ComponentRegistry` itself and spreads the whole member action
|
|
2010
|
+
onto that renderer's schema, so an author's `visible` on a member arrives as the
|
|
2011
|
+
child's own `schema.visible` and lands on the child's gate. `action:bar` is also
|
|
2012
|
+
the only gate on that path by design — its `filteredActions` deliberately
|
|
2013
|
+
filters on `requiredPermissions` and `actionRendersAt` only, leaving `visible` to
|
|
2014
|
+
the member renderer. The path is reachable end-to-end and is now pinned that way
|
|
2015
|
+
(registry-mounted `action:bar`, member declaring `visible: false`), so the
|
|
2016
|
+
reachability does not have to be argued again.
|
|
2017
|
+
|
|
2018
|
+
Both gates now read the same named definition as the rest of the family,
|
|
2019
|
+
`hasDeclaredVisibilityGate` (`!= null && !== ''`) — the invariant objectui#3492
|
|
2020
|
+
established for the selection bar and objectui#3758 applied to the row-action
|
|
2021
|
+
surfaces. The evaluation entry is untouched and already short-circuits a boolean
|
|
2022
|
+
rather than handing it to the CEL engine, which `actionPredicate.parity` pins for
|
|
2023
|
+
both the engine and the renderer path.
|
|
2024
|
+
|
|
2025
|
+
Behaviour change surface, deliberately narrow: only an `action:button` /
|
|
2026
|
+
`action:icon` whose `visible` is the literal boolean `false` (or another falsy
|
|
2027
|
+
non-empty value) changes — from rendered to hidden, which is what the
|
|
2028
|
+
declaration asked for. `visible: true` still renders, `''` and an absent
|
|
2029
|
+
`visible` are still no gate at all, and no expression-valued `visible` changes
|
|
2030
|
+
verdict. `ActionSchema.visible` is `ExpressionInputSchema` with no boolean
|
|
2031
|
+
member, so `objectstack build` cannot emit this shape; hand-written view JSON and
|
|
2032
|
+
in-process callers constructing action defs can.
|
|
2033
|
+
|
|
2034
|
+
The remaining three component-level gates (`action:group`, `action:menu`,
|
|
2035
|
+
`action:bar`'s own) stay as they are — they only ever mount through
|
|
2036
|
+
`SchemaRenderer`, which resolves `visible` first, and the overflow `action:menu`
|
|
2037
|
+
that `action:bar` synthesizes carries no `visible` at all.
|
|
2038
|
+
|
|
2039
|
+
- 993336f: An action declaring `disabled: ''` is no longer greyed out forever (objectui#3842)
|
|
2040
|
+
|
|
2041
|
+
The "is a `disabled` gate declared?" test stopped at `!= null`, missing the
|
|
2042
|
+
`!== ''` half of the invariant the `visible` family converged on
|
|
2043
|
+
(`hasDeclaredVisibilityGate`, objectui#3492 / #3758 / #3812 / #3823 / #3835). So
|
|
2044
|
+
`disabled: ''` counted as a declared gate, and the verdict went to the evaluation
|
|
2045
|
+
entry — which reads an empty predicate as "no condition → `true`"
|
|
2046
|
+
(`toPredicateInput('')` is `undefined`, `evaluateCondition(undefined)` is `true`).
|
|
2047
|
+
|
|
2048
|
+
The direction is why this half is a defect and the `visible` half was not. On
|
|
2049
|
+
`visible`, that `true` means SHOW, so an over-broad "declared" test and a
|
|
2050
|
+
permissive empty predicate cancel out and `visible: ''` renders either way. On
|
|
2051
|
+
`disabled`, the same `true` means DISABLE — the two mistakes compound, and an
|
|
2052
|
+
empty predicate stopped meaning "no gate" and started meaning "permanently
|
|
2053
|
+
greyed out". One empty predicate, opposite treatment under two keys.
|
|
2054
|
+
|
|
2055
|
+
Two gates now ask the shared definition instead:
|
|
2056
|
+
|
|
2057
|
+
- `@object-ui/app-shell`'s `DeclaredActionsBar` — the hot one. Its actions are
|
|
2058
|
+
SERVER-declared (`objectDef.actions[]`) and its hosts are the approvals inbox's
|
|
2059
|
+
record sections, so a `disabled: ''` arriving from metadata (an authoring form
|
|
2060
|
+
left empty, a template that rendered to an empty string) produced an Approve /
|
|
2061
|
+
Reject button nobody could click, indistinguishable from deliberate metadata.
|
|
2062
|
+
objectui#3835 was this same surface failing the other way.
|
|
2063
|
+
- `@object-ui/components`' `action:button` — verified to be the same shape before
|
|
2064
|
+
it was changed (the issue inferred it from the identical spelling but did not
|
|
2065
|
+
probe it): with `disabled: ''` the rendered button carried `disabled=""`.
|
|
2066
|
+
|
|
2067
|
+
**Behaviour change surface, deliberately narrow.** Only `disabled: ''` changes —
|
|
2068
|
+
from disabled to clickable, which is what "no predicate" asked for. `disabled:
|
|
2069
|
+
true` still disables, `disabled: false` and an absent `disabled` still do not, and
|
|
2070
|
+
no expression-valued `disabled` changes verdict. One consequence worth naming: on
|
|
2071
|
+
`action:button`, an empty `disabled` now falls THROUGH to the legacy non-spec
|
|
2072
|
+
`enabled` fallback instead of short-circuiting on the empty predicate, so an
|
|
2073
|
+
action spelling both (`disabled: ''` + `enabled: true`) becomes clickable.
|
|
2074
|
+
|
|
2075
|
+
The legacy `enabled` leg of `action:button` was routed through the same
|
|
2076
|
+
definition for consistency, and that part is behaviour-preserving by derivation
|
|
2077
|
+
rather than a fix: the leg is negated (`disabled = !isEnabled`), so an empty
|
|
2078
|
+
predicate's `true` already arrived as "not disabled" — the same verdict "no gate"
|
|
2079
|
+
produces. All four shapes are identical under either test; the derivation table
|
|
2080
|
+
and the reason no test can distinguish them are written down next to the pins.
|
|
2081
|
+
|
|
2082
|
+
`hasDeclaredVisibilityGate` keeps its historic name at both call sites (the
|
|
2083
|
+
objectui#3842 dispatch ruling): the predicate is key-neutral, and one
|
|
2084
|
+
implementation behind two names is how a repo grows dialects. Each call site says
|
|
2085
|
+
so in a comment.
|
|
2086
|
+
|
|
2087
|
+
- f0a625a: `disabled: ''` no longer greys out the remaining five action surfaces (objectui#3849)
|
|
2088
|
+
|
|
2089
|
+
objectui#3842 / PR #3851 fixed the "is a `disabled` gate DECLARED?" test on
|
|
2090
|
+
`action:button` and app-shell's `DeclaredActionsBar`. Five same-shaped sites were
|
|
2091
|
+
outside that PR's scope and stayed on `!= null`, so within one component the
|
|
2092
|
+
`visible` gate asked `hasDeclaredVisibilityGate` while the `disabled` gate on the
|
|
2093
|
+
next line asked `!= null` — two spellings of one question:
|
|
2094
|
+
|
|
2095
|
+
- `@object-ui/components` — `action:icon`, `action:group`'s inline button
|
|
2096
|
+
(`InlineActionButton`) and dropdown item (`DropdownActionItem`), and
|
|
2097
|
+
`action:menu`'s item (`ActionMenuItem`).
|
|
2098
|
+
- `@object-ui/plugin-detail` — `record:quick_actions`' `QuickActionButton`.
|
|
2099
|
+
|
|
2100
|
+
Why the missing `!== ''` half is a defect on this key and not on `visible`:
|
|
2101
|
+
`toPredicateInput('')` is `undefined` and `evaluateCondition(undefined)` is
|
|
2102
|
+
`true`. On `visible` that `true` means SHOW, so an over-broad "declared" test and
|
|
2103
|
+
a permissive empty predicate cancel out. On `disabled` it means DISABLE, so they
|
|
2104
|
+
compound — `disabled: ''` (an empty predicate: nothing declared) rendered a
|
|
2105
|
+
permanently greyed-out control, with nothing the author could write to un-grey
|
|
2106
|
+
it. Unlike #3842's approvals inbox, these five are the general action face
|
|
2107
|
+
(toolbars, dropdowns, record quick actions), so the reach is wider even though no
|
|
2108
|
+
single high-value host owns them.
|
|
2109
|
+
|
|
2110
|
+
**Behaviour change surface, deliberately narrow.** Only `disabled: ''` changes —
|
|
2111
|
+
from disabled to clickable, which is what "no predicate" asked for. `disabled:
|
|
2112
|
+
true` still disables, `disabled: false` and an absent `disabled` still do not, and
|
|
2113
|
+
no expression-valued `disabled` changes verdict. On the four sites that also carry
|
|
2114
|
+
the legacy non-spec `enabled` fallback, one consequence follows: an empty
|
|
2115
|
+
`disabled` now falls THROUGH to that leg instead of short-circuiting on the empty
|
|
2116
|
+
predicate, so an action spelling both (`disabled: ''` + `enabled: true`) becomes
|
|
2117
|
+
clickable. `record:quick_actions` has no `enabled` leg, so its chain is the single
|
|
2118
|
+
gate.
|
|
2119
|
+
|
|
2120
|
+
Routing those legacy `enabled` legs through the same definition is
|
|
2121
|
+
behaviour-preserving by derivation rather than a fix: the leg is negated
|
|
2122
|
+
(`disabled = !isEnabled`), so an empty predicate's `true` already arrived as "not
|
|
2123
|
+
disabled" — the verdict "no gate declared" produces. #3842's four-shape derivation
|
|
2124
|
+
table is reproduced next to the new pins, together with the statement that no
|
|
2125
|
+
`enabled` case can go red by reverting that leg.
|
|
2126
|
+
|
|
2127
|
+
`hasDeclaredVisibilityGate` keeps its historic name (the objectui#3842 ruling): the
|
|
2128
|
+
predicate is key-neutral, and one implementation behind two names is how a repo
|
|
2129
|
+
grows dialects. The three `@object-ui/components` sites import it relatively;
|
|
2130
|
+
`record:quick_actions` takes it from the package barrel, the cross-package route
|
|
2131
|
+
objectui#3835 opened. Every call site says so in a comment.
|
|
2132
|
+
|
|
2133
|
+
- b5980f4: Action-face member actions declaring `visible: false` are now hidden instead of rendered
|
|
2134
|
+
|
|
2135
|
+
The three member-action gates on the action face asked truthiness —
|
|
2136
|
+
`if (action.visible && !isVisible) return null` — so `visible: false`, the most
|
|
2137
|
+
explicit way an author can say "never show this", fell into the "no gate
|
|
2138
|
+
declared" branch and the action rendered anyway:
|
|
2139
|
+
|
|
2140
|
+
- `action:group` in `display: 'inline'` mode (`InlineActionButton`);
|
|
2141
|
+
- `action:group` in `display: 'dropdown'` mode (`DropdownActionItem`);
|
|
2142
|
+
- `action:menu`'s items (`ActionMenuItem`).
|
|
2143
|
+
|
|
2144
|
+
These leaves `.map()` the component's own `actions` array, so neither
|
|
2145
|
+
`SchemaRenderer`'s node-level `visible` handling nor
|
|
2146
|
+
`ActionEngine.getActionsForLocation` (whose boolean `visible` was always
|
|
2147
|
+
correct) is in the path — the truthy gate was the only gate.
|
|
2148
|
+
|
|
2149
|
+
All three now read one named definition, `hasDeclaredVisibilityGate`
|
|
2150
|
+
(`!= null && !== ''`), and let the declaration itself decide. This is not a new
|
|
2151
|
+
decision: objectui#3492 established the invariant for the selection bar, whose
|
|
2152
|
+
`hasVisibilityGate` spells out why truthiness cannot answer the question, and
|
|
2153
|
+
objectui#3758 applied it to both row-action surfaces. The evaluation entry is
|
|
2154
|
+
untouched and already short-circuits a boolean rather than handing it to the CEL
|
|
2155
|
+
engine, which `actionPredicate.parity` pins for both the engine and the renderer
|
|
2156
|
+
path.
|
|
2157
|
+
|
|
2158
|
+
Behaviour change surface, deliberately narrow: only a member action whose
|
|
2159
|
+
`visible` is the literal boolean `false` (or another falsy non-empty value)
|
|
2160
|
+
changes — from rendered to hidden, which is what the declaration asked for.
|
|
2161
|
+
`visible: true` still renders, `''` and an absent `visible` are still no gate at
|
|
2162
|
+
all, and no expression-valued `visible` changes verdict.
|
|
2163
|
+
`ActionSchema.visible` is `ExpressionInputSchema` with no boolean member, so
|
|
2164
|
+
`objectstack build` cannot emit this shape; hand-written view JSON and
|
|
2165
|
+
in-process callers constructing action defs can.
|
|
2166
|
+
|
|
2167
|
+
- 8aad9fd: Action-face predicates written against the canonical `record.` root now evaluate
|
|
2168
|
+
|
|
2169
|
+
`action:button`, `action:icon`, `action:menu` and `action:group` gated their
|
|
2170
|
+
actions on `useCondition(pred, context)`, which evaluates on
|
|
2171
|
+
`new ExpressionEvaluator({ ...scope, ...context })` — and the context each of
|
|
2172
|
+
them passed was the row spread flat, or nothing at all. Only the shorthand
|
|
2173
|
+
spelling resolved:
|
|
2174
|
+
|
|
2175
|
+
| predicate | verdict, before |
|
|
2176
|
+
| ---------------------------- | -------------------------------- |
|
|
2177
|
+
| `status == "pending"` | evaluates (`action:button` only) |
|
|
2178
|
+
| `record.status == "pending"` | throws `record is not defined` |
|
|
2179
|
+
| `data.status == "pending"` | throws `data is not defined` |
|
|
2180
|
+
|
|
2181
|
+
`record.` is not a mistaken spelling — it is the canonical one. It is what
|
|
2182
|
+
`ExpressionEvaluator`'s CEL path binds (`bag.record` as the record namespace),
|
|
2183
|
+
what `evalRowPredicate` binds on the record header, list rows, the row kebab
|
|
2184
|
+
and conditional formatting (`record.status` / bare `status` / `data.status`),
|
|
2185
|
+
and what the server enforces with. A `visible` that fails CLOSED turns the throw
|
|
2186
|
+
into "hidden", so a correctly-authored predicate deleted its own button —
|
|
2187
|
+
indistinguishable from the gate having said no. On the fail-soft legs the same
|
|
2188
|
+
throw lands the other way: `disabled` greyed a control out for everyone.
|
|
2189
|
+
|
|
2190
|
+
Live rather than theoretical: every declared action on framework's
|
|
2191
|
+
`sys_approval_request` gates on `record.viewer.*`, so the whole server-declared
|
|
2192
|
+
approval decision set was invisible wherever the declared-action bar rendered
|
|
2193
|
+
until objectui#4077 fixed that bar. These four generic renderers carried the
|
|
2194
|
+
same binding.
|
|
2195
|
+
|
|
2196
|
+
What changed:
|
|
2197
|
+
|
|
2198
|
+
- all four bind the row the three canonical ways, through one named helper
|
|
2199
|
+
(`usePredicateRecordContext`, exported from `@object-ui/react` beside
|
|
2200
|
+
`useCondition`), so the action face and the row surfaces answer an author's
|
|
2201
|
+
`visible:` the same way;
|
|
2202
|
+
- `action:icon` reads the row at all. It evaluated against an empty bag, so not
|
|
2203
|
+
even the bare-field shorthand resolved — and its `data` prop was landing in
|
|
2204
|
+
the props spread onto the DOM button;
|
|
2205
|
+
- `action:menu`'s items and `action:group`'s two leaves receive the row from
|
|
2206
|
+
their host, which they previously never got;
|
|
2207
|
+
- `action:bar` forwards the row into the overflow menu it builds, not just to
|
|
2208
|
+
its inline members. An action's predicate had been answering a different
|
|
2209
|
+
question purely because it spilled past `maxVisible` — which on mobile
|
|
2210
|
+
defaults to 1, making the verdict a function of the viewport.
|
|
2211
|
+
|
|
2212
|
+
Deliberately unchanged: the evaluation entry and each site's error policy. A
|
|
2213
|
+
predicate that genuinely faults still fails closed on `action:button` /
|
|
2214
|
+
`action:menu` `visible` and still fails soft on the other legs, exactly as
|
|
2215
|
+
before; `toPredicateInput`, `hasDeclaredVisibilityGate` and the empty-predicate
|
|
2216
|
+
rules keep their pinned semantics. Binding the row is a separate question from
|
|
2217
|
+
what to do when the predicate faults.
|
|
2218
|
+
|
|
2219
|
+
A surface with no row of its own binds nothing rather than an empty record, so
|
|
2220
|
+
a host that supplies the row through the ambient predicate scope is not blanked
|
|
2221
|
+
out; a row passed explicitly still wins over the scope.
|
|
2222
|
+
|
|
2223
|
+
- 0cbdca8: Built-in `select` fields: the form's label, validation message and required state now reach the control
|
|
2224
|
+
|
|
2225
|
+
A hand-written form field `{ name: 'status', label: 'Status', type: 'select', options: [...] }`
|
|
2226
|
+
rendered a visible "Status" label that pointed at nothing. Measured before the fix:
|
|
2227
|
+
`<label for="…-form-item">` resolved to no element at all, `getByLabelText(/status/i)`
|
|
2228
|
+
found zero matches, and the trigger button carried no `id`, no `aria-describedby`, no
|
|
2229
|
+
`aria-invalid` and no `aria-required`. Clicking the label did nothing and a screen reader
|
|
2230
|
+
announced an anonymous combobox with no field name, no error link and no required state.
|
|
2231
|
+
|
|
2232
|
+
Cause: the built-in `select` branch spread its whole DOM pass-through onto Radix's
|
|
2233
|
+
`Select.Root`. Root renders no DOM element of its own, so everything it does not
|
|
2234
|
+
recognise is silently dropped — and that is exactly where `<FormControl>`'s Slot puts the
|
|
2235
|
+
field's `id` / `aria-describedby` / `aria-invalid` and the renderer puts `aria-required`.
|
|
2236
|
+
The pass-through now lands on `SelectTrigger`, the focusable `button[role=combobox]` the
|
|
2237
|
+
user and their assistive tech actually operate, with the same two keys deliberately kept
|
|
2238
|
+
on Root as in the widget-side fix: `name` (the one key Root consumes — it forwards it to
|
|
2239
|
+
the hidden native `select` that takes part in form submission) and `disabled` (Root
|
|
2240
|
+
disables trigger, items and hidden select together, so the raw prop must not gain a second
|
|
2241
|
+
author). `ref` rides the pass-through too, so react-hook-form can finally focus a built-in
|
|
2242
|
+
select.
|
|
2243
|
+
|
|
2244
|
+
This is the built-in half of the same mechanism objectui#3306 fixed on the widget side.
|
|
2245
|
+
The two halves diverged because `'select'` is a `BUILTIN_FIELD_TYPES` member: an
|
|
2246
|
+
object-driven `field:select` resolves to the registered `SelectField` (fixed since #3306),
|
|
2247
|
+
while a hand-written bare `type: 'select'` never consults the registry and kept the
|
|
2248
|
+
defect. Both paths are now pinned side by side — the registered path as a positive control
|
|
2249
|
+
— so they cannot drift apart again. Which component renders a bare `select` is unchanged;
|
|
2250
|
+
only where its host props land.
|
|
2251
|
+
|
|
2252
|
+
Authored `className` on a built-in `select` now reaches the trigger (it previously reached
|
|
2253
|
+
no element), composed with the branch's touch-target height rather than replacing it.
|
|
2254
|
+
|
|
2255
|
+
- d3e738a: Export `hasDeclaredVisibilityGate` from the package barrel (objectui#3835)
|
|
2256
|
+
|
|
2257
|
+
`hasDeclaredVisibilityGate(visible)` — "did this action DECLARE a visibility gate
|
|
2258
|
+
at all?", i.e. `!= null && !== ''`, with the verdict left to the evaluation entry
|
|
2259
|
+
— is the single definition objectui#3492 established and PR #3816 / #3825 / #3836
|
|
2260
|
+
applied to every member-action gate in this package and in `@object-ui/plugin-grid`.
|
|
2261
|
+
It lived module-private in `src/renderers/action/visibility-gate.ts`.
|
|
2262
|
+
|
|
2263
|
+
The family turned out to have a member outside these packages:
|
|
2264
|
+
`@object-ui/app-shell`'s `DeclaredActionsBar` gates server-declared actions with
|
|
2265
|
+
the same question and had the same truthiness bug (objectui#3835). Exporting the
|
|
2266
|
+
one definition is what keeps that fix from becoming a fifth hand-spelled copy of
|
|
2267
|
+
it — the drift shape objectui#3142 already had to unpick for `locations` in these
|
|
2268
|
+
same files.
|
|
2269
|
+
|
|
2270
|
+
Additive only: one `export` line, no behaviour change in this package. The
|
|
2271
|
+
function is pure and dependency-free.
|
|
2272
|
+
|
|
2273
|
+
- c3b01a7: Give composite and grouped field widgets a real accessible name: the form renderer now associates its label by IDREF for widgets that declare `labelling: 'group'`, instead of emitting a `<label for>` that nothing labelable answers (objectui#3961).
|
|
2274
|
+
|
|
2275
|
+
Six widgets rendered a visible group label that named **nothing** in the accessibility tree. Measured in a real form, one field per row, reading each label's `for` against the DOM:
|
|
2276
|
+
|
|
2277
|
+
```
|
|
2278
|
+
address for=…-form-item -> MISSING byLabelText=0 role+name=0
|
|
2279
|
+
geolocation for=…-form-item -> MISSING byLabelText=0 role+name=0
|
|
2280
|
+
checkboxes for=…-form-item -> div byLabelText=0 role+name=0
|
|
2281
|
+
radio for=…-form-item -> div[radiogroup] byLabelText=0 role+name=0
|
|
2282
|
+
rating for=…-form-item -> div byLabelText=0 role+name=0
|
|
2283
|
+
file for=…-form-item -> div[role=button] byLabelText=0 role+name=0
|
|
2284
|
+
```
|
|
2285
|
+
|
|
2286
|
+
Two shapes, one outcome. `address` / `geolocation` spread the host's id onto their first sub-input and then replaced it with that input's own unique id (objectui#3343, correct in itself), so the `for` named an id no element carried — clicking "Shipping Address" did nothing and the group label was absent from the accessibility tree entirely. `checkboxes` / `radio` / `rating` / `file` kept the id, but on a `div`: a `<label for>` on a non-labelable element is inert HTML — `HTMLLabelElement.control` is `null`, so it activates nothing and contributes no name. A screen reader heard "Street Address", "City", "Alpha", "Beta" — never which group they belonged to.
|
|
2287
|
+
|
|
2288
|
+
The fix is the WAI-ARIA group pattern, driven by a DECLARATION rather than by the host guessing at widget DOM:
|
|
2289
|
+
|
|
2290
|
+
- `@object-ui/core` — `ComponentMeta` gains `labelling?: 'control' | 'group'`. Additive and optional; absent means `'control'`, which is every existing component's behaviour.
|
|
2291
|
+
- `@object-ui/components` — the form renderer reads it. For a `'group'` field the `<FormLabel>` publishes an `id` and drops its `for`, and the widget receives `aria-labelledby`. The single-control path is unchanged down to the attribute: no id on the label, no `aria-labelledby` key on the widget, so no field acquires a second naming channel. `ui/form.tsx` is untouched (Shadcn no-touch) — both halves travel as ordinary props, since `<FormLabel>` spreads props after its own `htmlFor`.
|
|
2292
|
+
- `@object-ui/fields` — the six audited widgets declare `labelling: 'group'`. `address` / `geolocation` move the host id (and only the id) from their first sub-input to the group container; `checkboxes` / `rating` answer a host-supplied `aria-labelledby` with `role="group"`; `radio` keeps Radix's more specific `radiogroup`; `file` takes the name on its dropzone with no invented group layer, because it has exactly one control that merely happens not to be labelable.
|
|
2293
|
+
|
|
2294
|
+
No new key in the widget props contract: `aria-*` is already declared on it and forwarded by `toDomProps`, the same channel `aria-required` (objectui#3290) travels.
|
|
2295
|
+
|
|
2296
|
+
Deliberately unchanged: sub-labels keep naming their own inputs (`aria-labelledby` overrides `<label for>`, so putting the group name on the first sub-input would have replaced "Street Address" with the field name — the concatenated-name outcome this issue rejected), `aria-describedby` stays on the first focusable sub-input where focus can reach it (objectui#3318), the sub-input ids of objectui#3343 do not move, and standalone rendering — the inline grid editor, a bare SDUI node, where nobody hands down an id and there is no host label to point at — emits no role and no IDREF at all.
|
|
2297
|
+
|
|
2298
|
+
A widget that does not declare itself keeps the old `for`, which the label-association tests report as an association resolving to a non-labelable element. Silence was the failure mode being fixed; the default path stays loud.
|
|
2299
|
+
|
|
2300
|
+
- 7ed3360: fix(data-table): don't render a row overflow ("⋮") trigger that opens an empty menu
|
|
2301
|
+
|
|
2302
|
+
The row overflow trigger was gated on whether row-action **handlers** were
|
|
2303
|
+
supplied (`onRowEdit` / `onRowDelete` / `rowActionDefs`), while the menu's items
|
|
2304
|
+
were filtered a second time — per item, per record — against
|
|
2305
|
+
`rowEditPredicates` / `rowDeletePredicates` and a custom action's `visible`. On a
|
|
2306
|
+
row where every item was predicate-suppressed the trigger still rendered and
|
|
2307
|
+
opened an empty box, which reads as a broken page.
|
|
2308
|
+
|
|
2309
|
+
The trigger is now decided by the items that will actually render for that row,
|
|
2310
|
+
resolved through the same visibility rule the items gate themselves on, so the
|
|
2311
|
+
two cannot disagree. The decision is per row: within one table a row that keeps
|
|
2312
|
+
an action keeps its trigger while a row with nothing left renders none. The
|
|
2313
|
+
actions cell itself is unchanged, so the column stays aligned with its header.
|
|
2314
|
+
|
|
2315
|
+
- 0fa5e4d: The `div` deprecation notice is now reported once per module load, not once per render (objectui#3965)
|
|
2316
|
+
|
|
2317
|
+
`DivRenderer` called `console.warn` on every render. That is invisible on a page
|
|
2318
|
+
with one `div` and destructive on a page with many: the docs schema-catalog index
|
|
2319
|
+
renders 400+ example thumbnails and emitted ~190 byte-identical notices, burying
|
|
2320
|
+
the page's real console errors underneath — the two nested-button errors fixed in
|
|
2321
|
+
objectui#3903 / PR #3964 had to be fished out of that flood, and it cost a
|
|
2322
|
+
browser-verification run its signal twice.
|
|
2323
|
+
|
|
2324
|
+
The deprecation itself is unchanged: dev builds still report it, the message and
|
|
2325
|
+
its migration guidance are byte-identical, and production builds are still
|
|
2326
|
+
silent. Only the repetition is gone. The guard is a module-level `Set` keyed by
|
|
2327
|
+
type, and the production early-return happens _before_ the set is marked, so a
|
|
2328
|
+
production render cannot suppress a later dev-build notice.
|
|
2329
|
+
|
|
2330
|
+
- 5bfaabd: `PageComponentSchema.dataSource` now reaches every object-bound block, not just
|
|
2331
|
+
`list-view` — and `element:record_picker` stops discarding `view`
|
|
2332
|
+
(objectstack#6953).
|
|
2333
|
+
|
|
2334
|
+
objectstack#5576 wired the spec's per-element data binding
|
|
2335
|
+
(`dataSource: { object, view?, filter?, sort?, limit? }`) to `list-view` and left
|
|
2336
|
+
the same declaration inert on every other page component. Two gaps remained, and
|
|
2337
|
+
both were silent:
|
|
2338
|
+
|
|
2339
|
+
- **`element:record_picker` read four of the five keys and dropped `view`.** So
|
|
2340
|
+
`dataSource: { object: 'account', view: 'hot' }` — the spec's own example —
|
|
2341
|
+
built a picker over EVERY account instead of the rows the saved view selects.
|
|
2342
|
+
Nothing threw and nothing rendered an error; the option list was simply wider
|
|
2343
|
+
than what was authored, which also means a user could select a record the page
|
|
2344
|
+
said was out of scope.
|
|
2345
|
+
- **`object-grid` / `object-form` / `object-kanban` / `object-calendar` /
|
|
2346
|
+
`object-chart` / `object-metric` / `record:related_list` read none of it.**
|
|
2347
|
+
Each gates its fetch on its own `objectName`, and nothing mapped
|
|
2348
|
+
`dataSource.object` onto it, so a page written the way the spec documents
|
|
2349
|
+
rendered an empty grid / a field-less form / a board with no cards / an empty
|
|
2350
|
+
month / an empty chart / a static metric number — with no request and no
|
|
2351
|
+
diagnostic anywhere. Spec-valid metadata rendering nothing is the
|
|
2352
|
+
objectstack#4413 shape.
|
|
2353
|
+
|
|
2354
|
+
Composition follows objectstack#5576's landed semantics unchanged on every block:
|
|
2355
|
+
a named saved view supplies the baseline, a key written on the component itself
|
|
2356
|
+
overrides it, an explicit binding key overrides both, `filter` AND-combines
|
|
2357
|
+
("additional filter criteria" — a binding can narrow a view, never widen it), and
|
|
2358
|
+
a `view` name that does not resolve renders a configuration error instead of
|
|
2359
|
+
degrading to the object's full scope.
|
|
2360
|
+
|
|
2361
|
+
- `@object-ui/react` — new `useElementDataSourceSchema(schema, mapping, dataSource?)`
|
|
2362
|
+
and `ElementDataSourceGate` apply a resolved binding to the schema keys a given
|
|
2363
|
+
block reads, plus `ElementDataSourceErrorPanel` / `ElementDataSourceLoadingPanel`
|
|
2364
|
+
for the two non-final states. One precedence table for all blocks rather than
|
|
2365
|
+
one copy per block — that copy is how "additional filter criteria" would have
|
|
2366
|
+
become two dialects.
|
|
2367
|
+
- A mapping names **only** keys its block genuinely reads. A composed value
|
|
2368
|
+
written onto a key the block ignores would be accepted and dropped, which is
|
|
2369
|
+
the defect being removed, one layer deeper — so a kanban's swimlane `columns`
|
|
2370
|
+
never receive a view's field list, and a block with no row cap leaves `limit`
|
|
2371
|
+
unmapped. The per-block coverage table, including two residual gaps that are
|
|
2372
|
+
named rather than papered over, is in `content/docs/guide/data-source.md`.
|
|
2373
|
+
|
|
2374
|
+
No behaviour changes for a block that carries no `dataSource`: the binding-free
|
|
2375
|
+
path returns the schema by reference, so nothing remounts and nothing refetches.
|
|
2376
|
+
|
|
2377
|
+
- ab3ad4f: An empty predicate is no longer a declared gate anywhere (objectui#3850, objectui#3862)
|
|
2378
|
+
|
|
2379
|
+
"Is a gate DECLARED on this key — is there a condition to reach a verdict on?" was
|
|
2380
|
+
answered three times in this repo, with three different scopes, and the widest
|
|
2381
|
+
answers sat on `disabled`, where the mistake is not benign:
|
|
2382
|
+
|
|
2383
|
+
- `hasDeclaredVisibilityGate` (the action face) asked `!= null && !== ''`, so every
|
|
2384
|
+
OBJECT counted — including `{ dialect: 'cel', source: '' }`. That envelope is not
|
|
2385
|
+
a hand-written curiosity: `@objectstack/spec`'s `ExpressionInputSchema` normalizes
|
|
2386
|
+
every authored predicate into one, so "the author left the predicate empty"
|
|
2387
|
+
compiles to exactly it. The verdict path normalized the same value back to
|
|
2388
|
+
`undefined`, and `evaluateCondition(undefined)` answers `true` — "no condition, so
|
|
2389
|
+
visible/enabled". On `visible` that `true` means SHOW, so the two mistakes
|
|
2390
|
+
cancelled; on `disabled` it means GREY, so they compounded: a button disabled
|
|
2391
|
+
forever that no author asked to disable (objectui#3850, the residue objectui#3842
|
|
2392
|
+
left behind).
|
|
2393
|
+
- `SchemaRenderer` asked `disabled !== undefined` inline, one notch wider again, so
|
|
2394
|
+
`disabled: null` greyed out too — on the GENERIC rendering path, since that block
|
|
2395
|
+
runs for every node type, and not as an internal flag either: `_disabled` is
|
|
2396
|
+
forwarded to the component as a real `disabled` prop (objectui#3862).
|
|
2397
|
+
- `ActionRunner`'s execution gates asked "does this normalize to something
|
|
2398
|
+
evaluable?" — the scope that turned out to be right (objectui#3848 / objectui#3872).
|
|
2399
|
+
|
|
2400
|
+
There is now ONE definition, `hasDeclaredPredicate`, exported from
|
|
2401
|
+
`@object-ui/core` (`evaluator/declaredPredicate.ts`, beside the `toPredicateInput`
|
|
2402
|
+
normalizer it is derived from): a gate is declared when normalization still leaves a
|
|
2403
|
+
condition to evaluate. `''`, a whitespace-only string, an empty-`source` envelope
|
|
2404
|
+
and any non-predicate value (`0`, `{}`) are NOT declared; `false` IS (a verdict is
|
|
2405
|
+
not a missing gate — objectui#3812). `hasDeclaredVisibilityGate` keeps its name as a
|
|
2406
|
+
re-export of it, so the five member-action renderer call sites, `DeclaredActionsBar`
|
|
2407
|
+
and `record-quick-actions` are unchanged and inherit the scope;
|
|
2408
|
+
`SchemaRenderer`'s `disabled` / `disabledOn` chain and `ActionRunner`'s two gates
|
|
2409
|
+
read the same function. No consumer got a local "and also check for empty" test —
|
|
2410
|
+
that fourth dialect is what objectui#3842 / objectui#3849 spent two PRs merging away.
|
|
2411
|
+
|
|
2412
|
+
Measured behaviour change, `action:button` and the generic path, before → after:
|
|
2413
|
+
|
|
2414
|
+
| value | `visible` | `disabled` | `enabled` | `SchemaRenderer` `disabled` prop |
|
|
2415
|
+
| --------------------------------------------------------- | -------------- | ---------- | --------- | -------------------------------- |
|
|
2416
|
+
| `''` | shown → shown | on → on | on → on | forwarded → absent |
|
|
2417
|
+
| `null` | shown → shown | on → on | on → on | forwarded → absent |
|
|
2418
|
+
| `{ dialect: 'cel', source: '' }` | shown → shown | GREY → on | on → on | forwarded → absent |
|
|
2419
|
+
| `{ source: '' }` | shown → shown | GREY → on | on → on | forwarded → absent |
|
|
2420
|
+
| `' '` (whitespace) | HIDDEN → shown | on → on | GREY → on | forwarded → absent |
|
|
2421
|
+
| `0` / `{}` (not predicates) | shown → shown | GREY → on | on → on | forwarded → absent |
|
|
2422
|
+
| `true` / `false` / bare CEL / `${…}` / non-empty envelope | unchanged | unchanged | unchanged | unchanged |
|
|
2423
|
+
|
|
2424
|
+
Every row moves toward "there is no gate here", never away from it, and no value
|
|
2425
|
+
that HAS a verdict changes it — the verdict is still read from the raw value, only
|
|
2426
|
+
the gate in front of it narrowed. Two rows are behaviour changes rather than the
|
|
2427
|
+
equivalence the ruling expected, and are pinned as such: the whitespace string moves
|
|
2428
|
+
on `visible` / `enabled` (it used to normalize to `'${ }'`, which evaluates falsy,
|
|
2429
|
+
so a predicate that says nothing HID the action from everyone), and non-predicate
|
|
2430
|
+
junk stops greying controls out (fail-open, the posture `ActionRunner` already
|
|
2431
|
+
committed to).
|
|
2432
|
+
|
|
2433
|
+
One blank spelling is knowingly still outside the scope: an envelope whose `source`
|
|
2434
|
+
is blank but not EMPTY (`{ dialect: 'cel', source: ' ' }`) — the normalizer folds a
|
|
2435
|
+
`source` of `''` and does not trim, so the string spelling of a blank predicate is
|
|
2436
|
+
trimmed and the envelope spelling is not, and `disabled` still greys out for that one
|
|
2437
|
+
value. The ruling enumerated three empty spellings; this is a fourth, measured and
|
|
2438
|
+
filed as objectui#3960 rather than widened in here.
|
|
2439
|
+
|
|
2440
|
+
One chain is deliberately untouched: `SchemaRenderer`'s `visible` / `visibleWhen` /
|
|
2441
|
+
`visibleOn` / `visibility` / `hidden` / `hiddenOn` legs keep `!== undefined`, because
|
|
2442
|
+
narrowing them would change ALIAS PRECEDENCE, not just emptiness. The `hidden` legs
|
|
2443
|
+
are not negated and therefore carry this same defect with the polarity that makes the
|
|
2444
|
+
node vanish — measured, out of this ruling's scope, filed as objectui#3955.
|
|
2445
|
+
|
|
2446
|
+
- c2fd122: fix(actions): forward `bodyShape` end-to-end so a declared body wrap is honoured
|
|
2447
|
+
|
|
2448
|
+
Sibling of the `bodyExtra` fix, same failure shape one key over. `bodyShape` is
|
|
2449
|
+
the spec's body-WRAPPING declaration for a `type: 'api'` action — `'flat'` (the
|
|
2450
|
+
default) or `{ wrap: key }` to nest the collected params under `key`, the shape
|
|
2451
|
+
better-auth's `organization/update` needs. The console `apiHandler` read it
|
|
2452
|
+
unconditionally while **no** action renderer forwarded it, so an author who
|
|
2453
|
+
declared `bodyShape: { wrap: 'data' }` on an `action:button` / `:group` / `:icon`
|
|
2454
|
+
/ `:menu` action got a FLAT body on the wire: the endpoint received the params at
|
|
2455
|
+
the top level, and the declaration read as honoured because it parsed and
|
|
2456
|
+
published.
|
|
2457
|
+
|
|
2458
|
+
The four declared-action renderers now forward the key, and `ActionSchema`
|
|
2459
|
+
declares it (typed by derivation from the spec, so the union cannot drift).
|
|
2460
|
+
`ActionRunner.executeAPI` — the fallback path taken when no host registered an
|
|
2461
|
+
`api` handler — now reads it too, closing a second asymmetry in which the same
|
|
2462
|
+
action changed body shape depending on which host executed it. The wrap covers
|
|
2463
|
+
the collected params only; `bodyExtra` and other top-level keys stay flat, which
|
|
2464
|
+
is the spec's own wording for the key and what both console read-sites already
|
|
2465
|
+
did.
|
|
2466
|
+
|
|
2467
|
+
`element:button` deliberately does **not** forward it: its whitelist mirrors
|
|
2468
|
+
spec's `InlineActionSchema` pick list field for field, and that pick list does
|
|
2469
|
+
not include `bodyShape` — it is not inline vocabulary.
|
|
2470
|
+
|
|
2471
|
+
- e24d767: Record page header action predicates now speak CEL, like every other action surface
|
|
2472
|
+
|
|
2473
|
+
`visible` / `hidden` / `disabled` on a `page:header` action were handed to
|
|
2474
|
+
ObjectUI's legacy JS evaluator, while the row kebab, the selection bar and
|
|
2475
|
+
conditional formatting have evaluated the identical metadata on the canonical
|
|
2476
|
+
CEL engine since objectui#1584 / ADR-0058. Every construct that exists only in
|
|
2477
|
+
CEL therefore worked in a list row and threw on the record page — where the
|
|
2478
|
+
throw fail-closed hid the button, leaving nothing on screen to notice:
|
|
2479
|
+
|
|
2480
|
+
- method calls — `record.f_tags.size() > 0`, `record.f_textarea.contains("x")`
|
|
2481
|
+
- the `in` operator — `'"red" in record.f_multiselect'` (a parse error)
|
|
2482
|
+
- stdlib functions — `record.f_date < today()` (`today is not a function`)
|
|
2483
|
+
|
|
2484
|
+
Both header evaluation sites now go through `evalRowPredicate`: the same entry,
|
|
2485
|
+
the same bindings (`record.*` + bare field names + `data.*` + the host scope,
|
|
2486
|
+
with relations bound as the stored foreign key), and the same fail-closed +
|
|
2487
|
+
warn-once semantics as the row surfaces. One predicate on one record now reaches
|
|
2488
|
+
the same show/hide verdict in the row menu, the selection bar and the record
|
|
2489
|
+
header.
|
|
2490
|
+
|
|
2491
|
+
Legacy-dialect strings are unaffected: `${…}`, `===`/`!==`, `?.`, `??` and
|
|
2492
|
+
JS-only methods such as `.includes()` still route to the legacy evaluator (with
|
|
2493
|
+
its existing one-time deprecation warning), so authored pages keep working. A
|
|
2494
|
+
`${…}` template predicate, which the header previously could not evaluate at
|
|
2495
|
+
all, now resolves through that fallback instead of hiding the button. A
|
|
2496
|
+
predicate that genuinely cannot be evaluated still hides its action, and now
|
|
2497
|
+
reports itself once in the same words the other surfaces use, naming the
|
|
2498
|
+
surface, the action and the predicate.
|
|
2499
|
+
|
|
2500
|
+
- aca561a: Four spec keys the renderers already honoured are now discoverable from the published `inputs`
|
|
2501
|
+
|
|
2502
|
+
`record:details.hideFields`, `record:related_list.relationshipValueField`,
|
|
2503
|
+
`record:related_list.add` and `element:text_input.defaultValue` were declared by
|
|
2504
|
+
`@objectstack/spec` and read by their renderers, while the registry `inputs` —
|
|
2505
|
+
the surface `gen-manifest.ts` serializes into `sdui.manifest.json` and
|
|
2506
|
+
`sdui-intrinsics.d.ts` — never mentioned them. Nothing anywhere reported the
|
|
2507
|
+
mismatch, and every layer that reads a manifest said the opposite of the
|
|
2508
|
+
runtime: the keys were in no designer panel and no generated `.d.ts`,
|
|
2509
|
+
`sdui-parser`'s prop walk returned `unknown-prop` for an author who wrote one,
|
|
2510
|
+
and the renderer honoured it regardless. That is objectui#3407's original
|
|
2511
|
+
complaint (`readonly` was enforced and honoured, the description just never said
|
|
2512
|
+
so) on four more keys.
|
|
2513
|
+
|
|
2514
|
+
Each description is derived from what the renderer actually does, not from
|
|
2515
|
+
restating the spec's one-liner, because the two can differ and the published
|
|
2516
|
+
text is what an AI author reads:
|
|
2517
|
+
|
|
2518
|
+
- `hideFields` documents bare field names only — the renderer tolerates
|
|
2519
|
+
`{name}` / `{field}` entries but the spec is `z.array(z.string())` and rejects
|
|
2520
|
+
them, so teaching that spelling would publish a dialect the contract refuses;
|
|
2521
|
+
- `relationshipValueField` publishes the renderer's `'id'` default and says that
|
|
2522
|
+
the resolved value drives the list filter, the Add-picker link value and the
|
|
2523
|
+
pre-filled create form together;
|
|
2524
|
+
- `add` publishes its member shape in prose (`ComponentInput` is flat and has no
|
|
2525
|
+
member-shape slot) with each default taken from the renderer — including
|
|
2526
|
+
`picker.labelField`, where the renderer defaults to `name` while the spec's
|
|
2527
|
+
own wording says "the object title field". It also names `picker.filter` as a
|
|
2528
|
+
KNOWN GAP rather than documenting it as a restriction: the spec declares it
|
|
2529
|
+
and nothing reads it, so an author would otherwise believe their picker is
|
|
2530
|
+
scoped when it offers every record (objectui#3831);
|
|
2531
|
+
- `defaultValue` distinguishes the two behaviours an author can get — seeding a
|
|
2532
|
+
bound page variable once while it is still empty, versus the native
|
|
2533
|
+
uncontrolled initial value with no variable bound.
|
|
2534
|
+
|
|
2535
|
+
`element:text_input` is not in the public tier, so its gap was not in
|
|
2536
|
+
`sdui.manifest.json` at all — it was in the JSX-page compiler's prop whitelist,
|
|
2537
|
+
which `renderers/layout/page.tsx` builds from `getKnownTypes()` plus these same
|
|
2538
|
+
`inputs`, making the undeclared `defaultValue` a live `unknown-prop` warning.
|
|
2539
|
+
|
|
2540
|
+
The repo-wide parity gate now runs in both directions over one covered set and
|
|
2541
|
+
one exemption discipline, so neither direction can be forgotten again the way
|
|
2542
|
+
the reverse half was after PR #3806. Nine spec keys stay deliberately
|
|
2543
|
+
unpublished, each with a written reason and a tracking issue: two the renderers
|
|
2544
|
+
do not read at all (objectui#3829), three retired upstream by ADR-0087
|
|
2545
|
+
tombstones, `page:tabs.type` (a carrier collision, objectstack#6776), two
|
|
2546
|
+
`targetVariable` declarative hints (objectui#3834), and
|
|
2547
|
+
`element:record_picker.filter` (objectui#3830).
|
|
2548
|
+
|
|
2549
|
+
- 0ef9dfd: `page:card` publishes `children` instead of the retired `body`, and `page:section` / `page:footer` / `page:sidebar` publish the `children` slot they render
|
|
2550
|
+
|
|
2551
|
+
`inputs` is the published authoring surface, not documentation: the Studio block
|
|
2552
|
+
designer builds its panel from it, `sdui-parser`'s `gen-manifest.ts` serializes
|
|
2553
|
+
it into `sdui.manifest.json` and `sdui-intrinsics.d.ts`, and the JSX-page
|
|
2554
|
+
compiler builds its prop whitelist from it. Two of the `page:*` containers had
|
|
2555
|
+
drifted from the contract in opposite directions.
|
|
2556
|
+
|
|
2557
|
+
`page:card` published `{ name: 'body', type: 'slot' }`. `@objectstack/spec`
|
|
2558
|
+
retired `PageCardProps.body` in objectstack#5775 (PR objectstack#6281, merged
|
|
2559
|
+
2026-08-07, ADR-0087 D2) and declared `children` in its place — one composition
|
|
2560
|
+
slot with one spelling, the same one `grid`, `flex`, `page:section` and
|
|
2561
|
+
`page:tabs` items already use. The designer was teaching a key the contract now
|
|
2562
|
+
rejects by name.
|
|
2563
|
+
|
|
2564
|
+
`page:section`, `page:footer` and `page:sidebar` declared no `inputs` at all, so
|
|
2565
|
+
the designer could not authorize the child list those three components exist to
|
|
2566
|
+
render. The same upstream PR replaced their `EmptyProps` declaration with the
|
|
2567
|
+
shared `PageContainerProps`, whose single key is `children`; all three now
|
|
2568
|
+
publish that one slot from one shared literal, mirroring the spec's own single
|
|
2569
|
+
definition.
|
|
2570
|
+
|
|
2571
|
+
Rendering is unchanged in both directions. `PageCardRenderer` still READS `body`
|
|
2572
|
+
first (`schema?.body ?? schema?.children`) and the three thin containers still
|
|
2573
|
+
read `schema?.children || schema?.body`, so documents stored under the old
|
|
2574
|
+
contract keep rendering until the ADR-0087 D2 conversion rewrites the key at
|
|
2575
|
+
load time — a back-compat read is not a second authorable spelling, the same
|
|
2576
|
+
split the `page-header-subtitle-alias` sequencing established. No validation
|
|
2577
|
+
verdict moves either: `children` is already in `sdui-parser`'s `BASE_PROPS` (so
|
|
2578
|
+
it was never an `unknown-prop`), `isContainer: true` was already set on all
|
|
2579
|
+
four, and `codegen.ts` filters `slot` inputs out of the generated `.d.ts` where
|
|
2580
|
+
`SduiBaseProps.children` types it.
|
|
2581
|
+
|
|
2582
|
+
What changes for an author is the designer surface: `body` is no longer offered
|
|
2583
|
+
on `page:card`, `children` is, and the three thin containers gained an
|
|
2584
|
+
authorable content slot.
|
|
2585
|
+
|
|
2586
|
+
- 7e5bb5d: fix(actions): forward `bodyExtra` end-to-end through the action chain
|
|
2587
|
+
|
|
2588
|
+
An action's static request body (`bodyExtra`) was dropped one hop before the
|
|
2589
|
+
`ActionRunner`: every action renderer forwards an explicit whitelist of keys, and
|
|
2590
|
+
none of them listed `bodyExtra`. Since `@objectstack/spec` 17 made it the only way
|
|
2591
|
+
a `type: 'api'` action can carry a payload (`params` keeps its single meaning as
|
|
2592
|
+
the parameter definition array), and the ADR-0087
|
|
2593
|
+
`inline-action-api-params-to-body-extra` conversion rewrites older object-form
|
|
2594
|
+
`params` pages onto it at load, a previously-working published page validated,
|
|
2595
|
+
published and then POSTed an empty body.
|
|
2596
|
+
|
|
2597
|
+
`element:button`, `action:button`, `action:group`, `action:icon` and `action:menu`
|
|
2598
|
+
now forward the key; `ActionRunner.executeAPI` merges it into the request body
|
|
2599
|
+
**last** (so a constant always overrides a same-named user param, matching the
|
|
2600
|
+
console `apiHandler`); `ActionSchema` declares it; and a non-array `params` on a
|
|
2601
|
+
`type: 'api'` action keeps working for one version window with a dev-mode
|
|
2602
|
+
deprecation warning naming `bodyExtra`.
|
|
2603
|
+
|
|
2604
|
+
- 54233b1: Record detail pages: a header ⟳ that refreshes the record, its related lists and its tab counts in place — no browser reload
|
|
2605
|
+
|
|
2606
|
+
Concurrent-editing scenario from the shop floor (MES work orders): operator A sits on a record's detail page while operator B starts or reports the same order. A had no way to see the new state except F5, which throws away the open tab, the scroll position and any in-progress inline edit along with the stale data.
|
|
2607
|
+
|
|
2608
|
+
The pipeline for this already existed — the objectui#2269 invalidation bus refetches every mounted reader in place, and `RecordContext.refresh` had been declared for it — but nothing produced that field and no UI reached for it. Three changes give it a trigger:
|
|
2609
|
+
|
|
2610
|
+
- **`RecordDetailView` produces `RecordContext.refresh`**, publishing `notifyDataChanged({ objectName: '*' })`. The wildcard is deliberate: a user reaches for refresh because of a write made by SOMEONE ELSE, which this client never saw and therefore cannot attribute to particular objects. `'*'` marks everything mounted as stale, so the main record, every related child list and the tab-count badges all refetch — no remount, so tab / scroll / draft state survive. First phase covers the standalone record route; embedded hosts (list drawer, split-pane preview) keep their existing chrome unchanged.
|
|
2611
|
+
- **`page:header` renders the ⟳** at the far end of the header row when — and only when — the host provides `refresh`. It is page chrome rather than a header action, so its position is the same on every record page regardless of which business actions the object declares, and it can never be collapsed into the `⋯` overflow. Styled as that `⋯` trigger's twin so the row reads as one button family. Its accessible name and tooltip come from the existing `common.refresh` key, so the icon-only button is not English-only in the other nine locales. The icon spins for a short floor after a click, because the bus is fire-and-forget and a warm backend would otherwise finish before the click looked like it landed.
|
|
2612
|
+
- **`RelatedList` accepts the `'*'` wildcard** on the legacy `objectui:related-changed` event, matching what `dataChangeMatches` already does for the bus's own readers. This listener compared the payload's object name to its own, so a wildcard invalidation reached everything on the page except the related lists — a concrete foreign object name is still ignored.
|
|
2613
|
+
|
|
2614
|
+
Hosts that provide no `refresh` render exactly as before.
|
|
2615
|
+
|
|
2616
|
+
- 97b63d7: Row actions declaring `visible: false` are now hidden instead of rendered
|
|
2617
|
+
|
|
2618
|
+
A custom row action's visibility **gate** was detected by truthiness, so
|
|
2619
|
+
`visible: false` — the most explicit way an author can say "never show this" —
|
|
2620
|
+
fell into the "no gate declared" branch and the action rendered for every row.
|
|
2621
|
+
Both surfaces of the ObjectGrid row cell (the "⋮" overflow item and the inline
|
|
2622
|
+
`variant:'primary'` button) and the data-table's row overflow menu read the same
|
|
2623
|
+
gate, so all three rendered it; the `#3562` emptiness guard counts with that same
|
|
2624
|
+
gate, so a row whose only action was `visible: false` also grew a "⋮" it could
|
|
2625
|
+
not fill.
|
|
2626
|
+
|
|
2627
|
+
The gate now detects a **declared** gate by `!= null && !== ''` and lets the
|
|
2628
|
+
declaration itself decide — a boolean short-circuits to its own verdict rather
|
|
2629
|
+
than being handed to the CEL engine. This is the invariant objectui#3492 already
|
|
2630
|
+
established for the selection bar, whose `hasVisibilityGate` spells out why
|
|
2631
|
+
truthiness cannot answer the question, and the same `!= null` posture the
|
|
2632
|
+
built-in `visibleWhen` gate has always had. `visible: true` still renders,
|
|
2633
|
+
`''` and an absent `visible` are still no gate at all, and no expression-valued
|
|
2634
|
+
`visible` changes verdict.
|
|
2635
|
+
|
|
2636
|
+
Behaviour change surface, deliberately narrow: only an action whose `visible` is
|
|
2637
|
+
the literal boolean `false` (or another falsy non-empty value) changes — it goes
|
|
2638
|
+
from rendered to hidden, which is what the declaration asked for.
|
|
2639
|
+
`ActionSchema.visible` is `ExpressionInputSchema` with no boolean member, so
|
|
2640
|
+
`objectstack build` cannot emit this shape; hand-written view JSON and
|
|
2641
|
+
in-process callers constructing defs can, and did. The three row surfaces now
|
|
2642
|
+
reach the same verdict as the selection bar and the record page header for every
|
|
2643
|
+
non-expression shape, which `predicate-surface-parity` pins.
|
|
2644
|
+
|
|
2645
|
+
- Updated dependencies [8aad9fd]
|
|
2646
|
+
- Updated dependencies [6719877]
|
|
2647
|
+
- Updated dependencies [56ff091]
|
|
2648
|
+
- Updated dependencies [7864f03]
|
|
2649
|
+
- Updated dependencies [d229dfa]
|
|
2650
|
+
- Updated dependencies [4bc6c23]
|
|
2651
|
+
- Updated dependencies [c3b01a7]
|
|
2652
|
+
- Updated dependencies [f5f8744]
|
|
2653
|
+
- Updated dependencies [69becd2]
|
|
2654
|
+
- Updated dependencies [5e52495]
|
|
2655
|
+
- Updated dependencies [b750823]
|
|
2656
|
+
- Updated dependencies [5bfaabd]
|
|
2657
|
+
- Updated dependencies [e06810e]
|
|
2658
|
+
- Updated dependencies [ab3ad4f]
|
|
2659
|
+
- Updated dependencies [c2fd122]
|
|
2660
|
+
- Updated dependencies [ac2139c]
|
|
2661
|
+
- Updated dependencies [b14ab3a]
|
|
2662
|
+
- Updated dependencies [8c60819]
|
|
2663
|
+
- Updated dependencies [e64a52e]
|
|
2664
|
+
- Updated dependencies [844d17f]
|
|
2665
|
+
- Updated dependencies [48132f7]
|
|
2666
|
+
- Updated dependencies [4dcd52a]
|
|
2667
|
+
- Updated dependencies [42ae5c6]
|
|
2668
|
+
- Updated dependencies [1d723e3]
|
|
2669
|
+
- Updated dependencies [0109f54]
|
|
2670
|
+
- Updated dependencies [7e5bb5d]
|
|
2671
|
+
- Updated dependencies [fbc23e0]
|
|
2672
|
+
- Updated dependencies [6d762da]
|
|
2673
|
+
- Updated dependencies [d11996e]
|
|
2674
|
+
- Updated dependencies [e6fdbdc]
|
|
2675
|
+
- Updated dependencies [f9faa7d]
|
|
2676
|
+
- Updated dependencies [6bb454a]
|
|
2677
|
+
- Updated dependencies [523be48]
|
|
2678
|
+
- Updated dependencies [7e2b7e9]
|
|
2679
|
+
- Updated dependencies [33526fd]
|
|
2680
|
+
- Updated dependencies [32413ec]
|
|
2681
|
+
- Updated dependencies [c1e1e6b]
|
|
2682
|
+
- @object-ui/react@17.4.0
|
|
2683
|
+
- @object-ui/core@17.4.0
|
|
2684
|
+
- @object-ui/i18n@17.4.0
|
|
2685
|
+
- @object-ui/types@17.4.0
|
|
2686
|
+
- @object-ui/react-runtime@17.4.0
|
|
2687
|
+
- @object-ui/sdui-parser@17.4.0
|
|
2688
|
+
|
|
3
2689
|
## 17.3.0
|
|
4
2690
|
|
|
5
2691
|
### Minor Changes
|
|
@@ -159,6 +2845,7 @@ props}` _alongside_ `field`. The predictable result was ~30 widgets resolving
|
|
|
159
2845
|
compile time rather than work under one host and not another.
|
|
160
2846
|
|
|
161
2847
|
## What did NOT change
|
|
2848
|
+
|
|
162
2849
|
- **Host metadata (SDUI JSON) is untouched.** No authored schema changes; this is
|
|
163
2850
|
a change to how widgets are _written_, not to what apps declare.
|
|
164
2851
|
- **`schema` is still the universal SDUI prop** every registered component
|
|
@@ -210,6 +2897,7 @@ props}` _alongside_ `field`. The predictable result was ~30 widgets resolving
|
|
|
210
2897
|
the flip where a name cannot.
|
|
211
2898
|
|
|
212
2899
|
## What changed
|
|
2900
|
+
|
|
213
2901
|
- Every field control — built-in (`input` / `textarea` / `checkbox` / `switch` /
|
|
214
2902
|
`select`) and registered widget alike — now receives `aria-required="true"`
|
|
215
2903
|
when the field resolves required, and **no attribute at all** when it does
|
|
@@ -253,7 +2941,7 @@ AriaAttributes`) and every widget forwards its leftover props to the control it
|
|
|
253
2941
|
half-translated in a way a user could see inside one widget: the dependency-gate
|
|
254
2942
|
sentence next to it already went through `t()`
|
|
255
2943
|
(`fields.options.selectFirst`), so under a `zh` session a gated select read
|
|
256
|
-
"请先选择Country" and the same select — one keystroke later, when the parent
|
|
2944
|
+
"请先选择 Country" and the same select — one keystroke later, when the parent
|
|
257
2945
|
value matched no option — flipped to English.
|
|
258
2946
|
|
|
259
2947
|
`fields.options.empty` is added to `useSafeFormTranslation`'s defaults map, the
|
|
@@ -357,6 +3045,7 @@ AriaAttributes`) and every widget forwards its leftover props to the control it
|
|
|
357
3045
|
- 展开按钮的无障碍名永远插值通用名词,一张有三个长文本字段的表单上三个按钮读屏完全一样。读屏用户无法判断自己要展开的是哪个字段,这是可达性缺陷而不是观感问题。
|
|
358
3046
|
|
|
359
3047
|
## 改了什么
|
|
3048
|
+
|
|
360
3049
|
- 调用点显式转发 `label`(与 `placeholder` / `emptyHint` 同法),这是唯一的行为改动。
|
|
361
3050
|
- `label` 属于 renderer-only:`stripRendererOnlyProps` 与 `stripRegisteredFieldProps` 各加一条丢弃项,所以它既不会变成 DOM 上的 `label="备注"` 杂属性(每个内置分支都会把剩余 props 直接摊到 DOM 节点上),也不会成为注册型 widget 新收到的 prop——自 v17 起 `field` 是它们唯一的元数据载体(objectui#3233),label 一直在那里读。
|
|
362
3051
|
- 内置 `textarea` 分支里那句 `const { label: _label, ...rest }` 随之删除。它本想拦住 label 落到 DOM,但既然从来没有 label 送进来,它拦的是不存在的东西(ESLint 一直报着 `'_label' is assigned a value but never used`),而且只护住了这一个分支。现在这件事由 strip 统一负责,所有分支同等受护。
|
|
@@ -2659,6 +5348,7 @@ overflow-auto` region (bottom on-screen, within the viewport) and the table can
|
|
|
2659
5348
|
objects+fields were editable in Studio; this reworks both surfaces.
|
|
2660
5349
|
|
|
2661
5350
|
**Setup (assign + read-only):**
|
|
5351
|
+
|
|
2662
5352
|
- The six facets (`object_permissions`, `field_permissions`, `system_permissions`,
|
|
2663
5353
|
`row_level_security`, `tab_permissions`, `admin_scope`) now render read-only on
|
|
2664
5354
|
the `sys_permission_set` record page as a compact summary (counts, or capability
|
|
@@ -2673,6 +5363,7 @@ overflow-auto` region (bottom on-screen, within the viewport) and the table can
|
|
|
2673
5363
|
|
|
2674
5364
|
**Studio (design every facet):** the permission matrix editor gains structured
|
|
2675
5365
|
editors for the facets that were JSON-only —
|
|
5366
|
+
|
|
2676
5367
|
- **System Capabilities**: a multi-select over the live `sys_capability` registry
|
|
2677
5368
|
(scope-grouped, labelled chips).
|
|
2678
5369
|
- **Row-Level Security**: per-policy rows (object · operation · enabled) with CEL
|
|
@@ -3409,6 +6100,7 @@ overflow-auto` region (bottom on-screen, within the viewport) and the table can
|
|
|
3409
6100
|
platform primitive for "no records / no data" states. Two new props
|
|
3410
6101
|
keep it flexible enough to absorb the hand-rolled variants that lived
|
|
3411
6102
|
in `plugin-list`, `plugin-kanban`, and `plugin-dashboard`:
|
|
6103
|
+
|
|
3412
6104
|
- `showIcon?: boolean` — drops the icon container entirely. Used by the
|
|
3413
6105
|
kanban board-level empty banner, which is a status banner rather than
|
|
3414
6106
|
a true empty-state.
|
|
@@ -3417,6 +6109,7 @@ overflow-auto` region (bottom on-screen, within the viewport) and the table can
|
|
|
3417
6109
|
empty state, which uses a large standalone glyph).
|
|
3418
6110
|
|
|
3419
6111
|
Adopters:
|
|
6112
|
+
|
|
3420
6113
|
- `plugin-list` (`ListView` grid empty-state) — preserves the existing
|
|
3421
6114
|
large icon, title, message, add-record button and `data-testid`s,
|
|
3422
6115
|
but delegates the structural markup to `DataEmptyState`.
|
|
@@ -3479,6 +6172,7 @@ overflow-auto` region (bottom on-screen, within the viewport) and the table can
|
|
|
3479
6172
|
The renderer now passes `schema.action` through `SchemaRenderer` to turn it
|
|
3480
6173
|
into a real React element, and explicitly strips `action`/`icon` from the
|
|
3481
6174
|
spread so schema-shaped objects don't reach DOM attributes.
|
|
6175
|
+
|
|
3482
6176
|
- @object-ui/types@5.1.1
|
|
3483
6177
|
- @object-ui/core@5.1.1
|
|
3484
6178
|
- @object-ui/i18n@5.1.1
|
|
@@ -3501,17 +6195,20 @@ overflow-auto` region (bottom on-screen, within the viewport) and the table can
|
|
|
3501
6195
|
- bd8447d: Three platform-wide detail polish items.
|
|
3502
6196
|
|
|
3503
6197
|
**Tighter page rhythm**
|
|
6198
|
+
|
|
3504
6199
|
- Outer `PageRenderer` padding `p-4 md:p-6 lg:p-8` → `p-3 md:p-4 lg:p-6`
|
|
3505
6200
|
and outer body wrap `space-y-8` → `space-y-6` so list / detail / home
|
|
3506
6201
|
pages share the same edge rhythm. Cuts ~16px of edge slack on lg.
|
|
3507
6202
|
|
|
3508
6203
|
**Highlights KPI treatment**
|
|
6204
|
+
|
|
3509
6205
|
- `HeaderHighlight` now renders numeric / currency / percent / decimal
|
|
3510
6206
|
values as KPI numbers (`text-xl md:text-2xl font-semibold tabular-nums`)
|
|
3511
6207
|
instead of the uniform `text-sm font-semibold`, so amount / probability
|
|
3512
6208
|
/ count fields read as headline stats — Salesforce-style key facts.
|
|
3513
6209
|
|
|
3514
6210
|
**Discussion footer upgrade**
|
|
6211
|
+
|
|
3515
6212
|
- `RecordActivityTimeline` now uses `RichTextCommentInput` (bold / italic /
|
|
3516
6213
|
list / code, `@`-mention autocomplete, preview toggle, Send) instead of
|
|
3517
6214
|
a bare `<textarea>`.
|
|
@@ -3526,6 +6223,7 @@ overflow-auto` region (bottom on-screen, within the viewport) and the table can
|
|
|
3526
6223
|
accordion in flush mode dropped all per-item borders so the collapsed
|
|
3527
6224
|
"Quotes / Products / Open Tasks" triggers stacked with zero visual
|
|
3528
6225
|
separation.
|
|
6226
|
+
|
|
3529
6227
|
- `@object-ui/plugin-detail` `DetailSection`: override the `CardTitle`
|
|
3530
6228
|
className to `text-base font-semibold tracking-tight`, slim down
|
|
3531
6229
|
`CardHeader` padding (`py-3 px-4 sm:py-4 sm:px-6`) and `CardContent`
|
|
@@ -3540,6 +6238,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
3540
6238
|
CRM-style related-list lists).
|
|
3541
6239
|
|
|
3542
6240
|
- d51a577: feat(platform): Discussion attachments + @mention directory + Reference Rail aside
|
|
6241
|
+
|
|
3543
6242
|
- **Discussion attachments** — `RichTextCommentInput` now accepts an `extraSlot`
|
|
3544
6243
|
and a `canSubmitEmpty` flag so hosts can mount the existing
|
|
3545
6244
|
`CommentAttachment` composer beneath the editor without forking the toolbar.
|
|
@@ -3559,6 +6258,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
3559
6258
|
- d1ec6a2: Fold inline-edit into the page-header overflow menu (HubSpot/Lightning
|
|
3560
6259
|
pattern) and remove the orphan "Edit fields" toolbar row that previously
|
|
3561
6260
|
floated between the tab strip and the first detail section.
|
|
6261
|
+
|
|
3562
6262
|
- `@object-ui/app-shell` `RecordDetailView`: injects a new `sys_inline_edit`
|
|
3563
6263
|
system action that appears in the ⋯ overflow menu and dispatches a
|
|
3564
6264
|
`objectui:record:inline-edit-toggle` window CustomEvent (filtered by
|
|
@@ -3575,6 +6275,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
3575
6275
|
in `page:accordion` / `page:tabs` items.
|
|
3576
6276
|
|
|
3577
6277
|
- d548d6b: Unify empty-state visuals across timeline + registered `empty` renderer.
|
|
6278
|
+
|
|
3578
6279
|
- `RecordActivityTimeline` and `ActivityTimeline` now use `DataEmptyState`
|
|
3579
6280
|
instead of a bare `<p>` so empty timelines match list/related-list visuals
|
|
3580
6281
|
(muted icon badge + centered copy).
|
|
@@ -3625,6 +6326,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
3625
6326
|
not only contacts of that account).
|
|
3626
6327
|
|
|
3627
6328
|
Two coupled fixes:
|
|
6329
|
+
|
|
3628
6330
|
1. `RelatedList` now requires `parentId` + `referenceField` to auto-
|
|
3629
6331
|
fetch. When both are present it calls `dataSource.find(api,
|
|
3630
6332
|
{ $filter: { [referenceField]: parentId } })`. When either is
|
|
@@ -3653,6 +6355,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
3653
6355
|
default detail view. They were missing cross-cutting affordances and
|
|
3654
6356
|
shipped with English-only tab labels and heavy bordered section cards
|
|
3655
6357
|
even when the host locale was Chinese. Track 1 closes the visible gap:
|
|
6358
|
+
|
|
3656
6359
|
- **app-shell `RecordDetailView`**: the `assignedPage` branch now wears
|
|
3657
6360
|
the same chrome as the default branch — lifecycle managed-by badge
|
|
3658
6361
|
and presence avatars in the top-right, `MetadataPanel` debug panel,
|
|
@@ -3681,6 +6384,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
3681
6384
|
record detail page (lead, opportunity, future account/contact/case).
|
|
3682
6385
|
Before this change it had two problems that bled into every custom
|
|
3683
6386
|
page across the product:
|
|
6387
|
+
|
|
3684
6388
|
1. **Quadruple registration**: `@object-ui/layout` registered both
|
|
3685
6389
|
`page-header` and `page:header`, and `@object-ui/components`
|
|
3686
6390
|
independently registered `page:header` (and `page:section`).
|
|
@@ -3691,6 +6395,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
3691
6395
|
than the default detail view it was meant to supersede.
|
|
3692
6396
|
|
|
3693
6397
|
This commit:
|
|
6398
|
+
|
|
3694
6399
|
- Removes the `@object-ui/layout` `page:header` registration. The
|
|
3695
6400
|
layout package keeps the legacy kebab-cased `page-header` alias only.
|
|
3696
6401
|
The canonical renderer now lives in `@object-ui/components` and is
|
|
@@ -3716,6 +6421,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
3716
6421
|
opportunity highlights strip looks coherent with the chip above it.
|
|
3717
6422
|
|
|
3718
6423
|
- ddb08a7: feat(page:header,page:tabs): title fallback + single-tab strip auto-hide (Phase G slice 3 polish)
|
|
6424
|
+
|
|
3719
6425
|
- `page:header.resolvedTitle` now honors `objectSchema.titleFormat`
|
|
3720
6426
|
(e.g. `{first_name} {last_name}`) and falls back through `name →
|
|
3721
6427
|
full_name → title → subject → display_name → label` before degrading
|
|
@@ -3762,6 +6468,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
3762
6468
|
muted badge on every empty Activity/History tab).
|
|
3763
6469
|
|
|
3764
6470
|
- b14fe09: Phase P.0 + P.5: tighten record-detail header chrome.
|
|
6471
|
+
|
|
3765
6472
|
- `RecordTitleChip` collapses the title row to a single baseline-aligned line — H1, eyebrow object label, copy-id, favorite star — instead of the previous two-row title + subtitle layout.
|
|
3766
6473
|
- `record:details` extends the highlight-field dedup set to also exclude the title field resolved from `objectSchema.primaryField` (or the standard `name`/`full_name`/`title`/`subject`/`display_name`/`label` fallbacks). Removes the duplicate row that previously echoed the H1 (e.g. "客户名称: Acme Corporation") inside the field grid.
|
|
3767
6474
|
|
|
@@ -3778,6 +6485,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
3778
6485
|
underline when active. 'card' and 'pill' variants are unchanged.
|
|
3779
6486
|
|
|
3780
6487
|
- 74962b0: feat(detail): record:discussion schema component + flush accordion variant
|
|
6488
|
+
|
|
3781
6489
|
- New `record:discussion` schema type lets authors place the record
|
|
3782
6490
|
chatter feed anywhere in a custom Page schema. Wired through a
|
|
3783
6491
|
shared `DiscussionContext` provider on the `assignedPage` branch
|
|
@@ -3804,6 +6512,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
3804
6512
|
accordion, discussion slot) that custom Lightning pages already enjoy.
|
|
3805
6513
|
|
|
3806
6514
|
Changes:
|
|
6515
|
+
|
|
3807
6516
|
- `buildDefaultPageSchema` now emits `page:tabs.items` (correct shape
|
|
3808
6517
|
for the renderer) rather than `tabs`.
|
|
3809
6518
|
- `PageHeaderRenderer.resolvedTitle` honors `objectSchema.primaryField`
|
|
@@ -3917,6 +6626,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
3917
6626
|
For every tab item whose `count` is not set explicitly, the renderer walks the tab's children (depth-first) to find the first `record:related_list` schema node and issues a `limit:1` find through the active `dataSource` to read the matching `total`. The badge appears in the tab strip without spec authors having to wire counts manually.
|
|
3918
6627
|
|
|
3919
6628
|
Behavior:
|
|
6629
|
+
|
|
3920
6630
|
- Explicit `count` in the spec always wins.
|
|
3921
6631
|
- Probe is filtered by the parent record id via `relationshipField` when present (skipped until the parent record is loaded).
|
|
3922
6632
|
- Best-effort: a failed probe just omits the badge — no error surface.
|
|
@@ -3957,6 +6667,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
3957
6667
|
### Patch Changes
|
|
3958
6668
|
|
|
3959
6669
|
- 6b683c8: fix(detail): clean up record page rendering
|
|
6670
|
+
|
|
3960
6671
|
- Drop `ai:chat_window` from the protocol-component placeholder list. The
|
|
3961
6672
|
floating chat overlay (plugin-chatbot) is the canonical AI entry point;
|
|
3962
6673
|
inline page schemas that still reference `ai:chat_window` now surface
|
|
@@ -3984,6 +6695,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
3984
6695
|
The report configuration panel is now safe to open on any spec-shape `Report` and only exposes fields that are actually persisted by `@objectstack/spec`.
|
|
3985
6696
|
|
|
3986
6697
|
`@object-ui/plugin-report`:
|
|
6698
|
+
|
|
3987
6699
|
- Add a bidirectional `SpecFilterAdapter` so `ReportConfigPanel` can edit
|
|
3988
6700
|
spec `FilterCondition` filters (`{field: value}`, `{field: {$op: value}}`,
|
|
3989
6701
|
top-level `$and`/`$or`). Complex / nested filters fall back to a
|
|
@@ -4005,15 +6717,18 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
4005
6717
|
- 18 new unit tests cover the filter adapter round-trip.
|
|
4006
6718
|
|
|
4007
6719
|
`@object-ui/components`:
|
|
6720
|
+
|
|
4008
6721
|
- `FilterBuilder` now guards against malformed external `value` props.
|
|
4009
6722
|
Previously a spec-shape filter (`{is_active: true}`) would crash the
|
|
4010
6723
|
component on first render; the builder now falls back to an empty
|
|
4011
6724
|
AND group whenever `value` is not a valid `FilterGroup`.
|
|
4012
6725
|
|
|
4013
6726
|
`@object-ui/i18n`:
|
|
6727
|
+
|
|
4014
6728
|
- Add `report.editor.*` strings to `en` and `zh`.
|
|
4015
6729
|
|
|
4016
6730
|
- 8442c05: Improve report editor panel usability based on real-user browser testing:
|
|
6731
|
+
|
|
4017
6732
|
- **Wider config panel** — the report editor now defaults to a `--config-panel-width`
|
|
4018
6733
|
of 440px (up from 280px), driven by a new optional `style` prop on
|
|
4019
6734
|
`ConfigPanelRenderer`. Long field labels, report titles, type labels, and filter
|
|
@@ -4136,6 +6851,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
4136
6851
|
detail view (`TypeError: titleFormat.replace is not a function`) and printed
|
|
4137
6852
|
`Failed to evaluate expression: ${[object Object]}` for every action visibility
|
|
4138
6853
|
predicate.
|
|
6854
|
+
|
|
4139
6855
|
- `@object-ui/core`: `ExpressionEvaluator.evaluate` / `evaluateCondition` now
|
|
4140
6856
|
unwrap Expression envelopes transparently.
|
|
4141
6857
|
- `@object-ui/react`: new `toPredicateInput()` helper to safely normalize
|
|
@@ -4218,6 +6934,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
4218
6934
|
plugin-designer, plugin-detail, plugin-editor, plugin-form, plugin-gantt,
|
|
4219
6935
|
plugin-grid, plugin-kanban, plugin-list, plugin-map, plugin-markdown,
|
|
4220
6936
|
plugin-report, plugin-timeline, plugin-view, plugin-workflow.
|
|
6937
|
+
|
|
4221
6938
|
- @object-ui/types@4.0.5
|
|
4222
6939
|
- @object-ui/core@4.0.5
|
|
4223
6940
|
- @object-ui/i18n@4.0.5
|
|
@@ -4232,6 +6949,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
4232
6949
|
Library builds (vite lib mode) now externalize every non-relative import instead of bundling third-party CJS dependencies into the published dist. This avoids inlined `require("react")` / `require("react-dom")` calls that cause `Calling \`require\` for "react" in an environment that doesn't expose the \`require\` function` runtime errors when consumer apps re-bundle the published dist.
|
|
4233
6950
|
|
|
4234
6951
|
Specifically fixes:
|
|
6952
|
+
|
|
4235
6953
|
- `@object-ui/plugin-dashboard` no longer inlines `react-grid-layout` (and its transitive `react-draggable` / `react-resizable` CJS bundles). `react-grid-layout` is now declared as a peer dependency so consumers install a single ESM-friendly copy.
|
|
4236
6954
|
- `@object-ui/components`, `@object-ui/plugin-calendar`, `@object-ui/plugin-charts`, `@object-ui/plugin-designer` no longer inline `react-i18next` / `i18next` / `use-sync-external-store` CJS shims.
|
|
4237
6955
|
- All plugin packages now use a unified `external: (id) => !/^[./]/.test(id) && !id.startsWith(__dirname)` rule, ensuring future additions of CJS deps are automatically externalized.
|
|
@@ -4288,6 +7006,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
4288
7006
|
contract through the renderer end-to-end:
|
|
4289
7007
|
|
|
4290
7008
|
**`@object-ui/types`** — `DataSource` gains four optional methods:
|
|
7009
|
+
|
|
4291
7010
|
- `createExportJob(resource, request)` → `{ jobId, status, estimatedRecords, createdAt }`
|
|
4292
7011
|
- `getExportJobProgress(jobId)` → `{ status, processedRecords, totalRecords, percentComplete, downloadUrl, … }`
|
|
4293
7012
|
- `cancelExportJob(jobId)` (optional)
|
|
@@ -4297,6 +7016,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
4297
7016
|
remain dependency-free.
|
|
4298
7017
|
|
|
4299
7018
|
**`@object-ui/components`** — new public API:
|
|
7019
|
+
|
|
4300
7020
|
- `useExportJob({ dataSource, pollIntervalMs, onComplete, onError })` — owns the
|
|
4301
7021
|
full polling loop, terminal-state handling, cancel, and download.
|
|
4302
7022
|
- `<ExportProgressDialog open onOpenChange job filename closeAfterDownloadMs />` —
|
|
@@ -4315,24 +7035,29 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
4315
7035
|
- a2d7023: End-user feature batch — forms, designer history, import/export, and PWA offline sync.
|
|
4316
7036
|
|
|
4317
7037
|
**Forms (`@object-ui/fields`, `@object-ui/providers`)**
|
|
7038
|
+
|
|
4318
7039
|
- `FileField`: native `<input capture="environment">` camera capture for mobile devices, plus a uploading-progress indicator driven by `UploadProvider`.
|
|
4319
7040
|
- `ImageField`: per-image inline crop/rotate via the lazy-loaded `ImageCropperDialog` (canvas-based, zero new deps).
|
|
4320
7041
|
- New `UploadProvider` in `@object-ui/providers` with pluggable adapters for S3 and Azure Blob (plus the default object-URL adapter for local previews). XHR-based with progress, abort, and retry.
|
|
4321
7042
|
- `LookupField`: `lookup.dependsOn: string | string[]` to chain dependent lookups (e.g. State depends on Country); the trigger is gated until parent values are present and the OData `$filter` is built automatically.
|
|
4322
7043
|
|
|
4323
7044
|
**Container-aware widget widths (`@object-ui/components`)**
|
|
7045
|
+
|
|
4324
7046
|
- New `useResizeObserver(ref)` hook exposing `{ width, height }` of any element. SSR-safe; reads the initial size via `getBoundingClientRect`.
|
|
4325
7047
|
- `plugin-gantt` and `plugin-kanban` now react to their container size instead of `window.innerWidth`, so they behave correctly inside split panels and dashboards.
|
|
4326
7048
|
|
|
4327
7049
|
**Designer history (`@object-ui/plugin-designer`)**
|
|
7050
|
+
|
|
4328
7051
|
- `useUndoRedo` (and therefore `useDesignerHistory`) gains `persistKey` + `storage` options to round-trip the undo/redo stack through `sessionStorage`, plus a `clearPersisted()` cleanup helper. Drafts now survive accidental tab refreshes.
|
|
4329
7052
|
- New `<HistoryPanel>` component renders the timeline visually with one-click jump-to-checkpoint via the new `jumpTo(index)` API.
|
|
4330
7053
|
|
|
4331
7054
|
**Import wizard (`@object-ui/plugin-grid`)**
|
|
7055
|
+
|
|
4332
7056
|
- Saved column-mapping templates: name, save, re-apply, and delete via a new template bar in the mapping step. Persisted under `objectui:import-templates:${objectName}` (override via `templateStorageKey` / `templateStorage`).
|
|
4333
7057
|
- Inline validation correction: cells with errors in the preview step are now editable; corrections feed straight into the import without requiring a re-upload, with green-bar status indicators for fixed rows.
|
|
4334
7058
|
|
|
4335
7059
|
**PWA offline sync (`@object-ui/mobile`)**
|
|
7060
|
+
|
|
4336
7061
|
- New `MemoryOfflineQueue` / `IndexedDbOfflineQueue` (`createOfflineQueue()` picks the best backend) backed by IndexedDB.
|
|
4337
7062
|
- `createOfflineDataSource(inner, { queue })` wraps any DataSource so mutations issued while offline (or that fail with a network-style error) are queued and replayed in order on reconnect. Includes `replay()`, `drop()`, `clear()`, `pending()`, an `onChange` notifier, and an opt-in `resolveConflict` hook for stale-write conflicts.
|
|
4338
7063
|
- New `useOfflineSync(source)` hook exposes `{ isOnline, pending, isReplaying, replay, drop, clear }` and auto-replays on the browser's `online` event.
|
|
@@ -4360,11 +7085,13 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
4360
7085
|
`FormSchema.mobileStickyActions` (new) is the lower-level escape hatch — applied automatically when `mobile.stickyActions` is set on `ObjectFormSchema`.
|
|
4361
7086
|
|
|
4362
7087
|
**`@object-ui/plugin-form`** — `ObjectForm` now:
|
|
7088
|
+
|
|
4363
7089
|
- propagates `mobile.fullscreenLongText` to every textarea/markdown/html field as `mobile_fullscreen: true`,
|
|
4364
7090
|
- sets `mobileStickyActions` on the inner form schema and adds `pb-20` padding so content isn't covered by the fixed bar,
|
|
4365
7091
|
- when `mobile.stepper === true` (or `'auto'` + `useIsMobile()` + > `stepperMinFields` fields), routes the flat field list through the existing `WizardForm` with synthetic single-field "steps" — keeping per-step validation and the existing `Next`/`Back`/`Submit` flow.
|
|
4366
7092
|
|
|
4367
7093
|
**`@object-ui/components`** — the registered `form` renderer adds:
|
|
7094
|
+
|
|
4368
7095
|
- a `mobileStickyActions` opt-in that turns the action row into a `position: sticky; bottom: 0` bar on small viewports, and
|
|
4369
7096
|
- an inline `FullscreenTextarea` wrapper used when no field-package widget is registered, providing the same expand-button + edit-dialog UX so the feature works even in lighter setups.
|
|
4370
7097
|
|
|
@@ -4403,6 +7130,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
4403
7130
|
`@object-ui/app-shell` and `@object-ui/components` have been updated to
|
|
4404
7131
|
point at the new paths; no runtime behaviour changed. A new
|
|
4405
7132
|
`examples/README.md` provides a "which example should I use?" selector.
|
|
7133
|
+
|
|
4406
7134
|
- @object-ui/types@3.3.1
|
|
4407
7135
|
- @object-ui/core@3.3.1
|
|
4408
7136
|
- @object-ui/i18n@3.3.1
|
|
@@ -4547,6 +7275,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
4547
7275
|
- New plugin-object and ObjectQL SDK updates
|
|
4548
7276
|
|
|
4549
7277
|
**Added:**
|
|
7278
|
+
|
|
4550
7279
|
- New Plugin: @object-ui/plugin-object - ObjectQL plugin for automatic table and form generation
|
|
4551
7280
|
- ObjectTable: Auto-generates tables from ObjectQL object schemas
|
|
4552
7281
|
- ObjectForm: Auto-generates forms from ObjectQL object schemas with create/edit/view modes
|
|
@@ -4555,6 +7284,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
4555
7284
|
- ObjectQL Integration: Enhanced ObjectQLDataSource with getObjectSchema() method using MetadataApiClient
|
|
4556
7285
|
|
|
4557
7286
|
**Changed:**
|
|
7287
|
+
|
|
4558
7288
|
- Updated @objectql/sdk from ^1.8.3 to ^1.9.1
|
|
4559
7289
|
- Updated @objectql/types from ^1.8.3 to ^1.9.1
|
|
4560
7290
|
|
|
@@ -4570,6 +7300,7 @@ full_name → title → subject → display_name → label` before degrading
|
|
|
4570
7300
|
- Patch release: Add automated changeset workflow and CI/CD improvements
|
|
4571
7301
|
|
|
4572
7302
|
This release includes infrastructure improvements:
|
|
7303
|
+
|
|
4573
7304
|
- Added changeset-based version management
|
|
4574
7305
|
- Enhanced CI/CD workflows with GitHub Actions
|
|
4575
7306
|
- Improved documentation for contributing and releasing
|