@object-ui/components 17.1.0 → 17.3.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 +859 -0
- package/dist/custom/combobox.d.ts +12 -2
- package/dist/custom/field.d.ts +20 -2
- package/dist/custom/filter-builder.d.ts +20 -4
- package/dist/custom/navigation-overlay.d.ts +7 -1
- package/dist/custom/sort-builder.d.ts +14 -3
- package/dist/index.css +6 -0
- package/dist/index.js +3371 -3223
- package/dist/index.umd.cjs +6 -6
- package/dist/lib/close-label.d.ts +50 -0
- package/dist/share/ShareDialog.d.ts +23 -27
- package/package.json +17 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,864 @@
|
|
|
1
1
|
# @object-ui/components
|
|
2
2
|
|
|
3
|
+
## 17.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3889ffb: Console chrome i18n gaps (objectstack#5407).
|
|
8
|
+
|
|
9
|
+
- A dependency-gated lookup now names its controlling field by its **label**
|
|
10
|
+
instead of its raw API name. The sentence was localized but the interpolated
|
|
11
|
+
name was not, so every locale — English included — read `Select crm_account
|
|
12
|
+
first`. The form renderer passes a new `dependsOnLabels` widget prop (the
|
|
13
|
+
lookup-side counterpart of `emptyHint`, which it already resolves to labels
|
|
14
|
+
for the fixed-option widgets); a name the host does not cover still falls
|
|
15
|
+
back to itself.
|
|
16
|
+
- The page-header overflow trigger's `More actions` accessible name now reads
|
|
17
|
+
`detail.moreActions`, the same key `action:menu`'s own overflow trigger uses,
|
|
18
|
+
so the two cannot diverge per locale.
|
|
19
|
+
- The activity-feed reaction button's `Add reaction` accessible name is now a
|
|
20
|
+
bundle key (`detail.addReaction`, added to all ten packs).
|
|
21
|
+
- The "check the highlighted fields" toast joins field names with a per-locale
|
|
22
|
+
separator (`validation.formInvalidJoiner`) instead of a hardcoded `、`
|
|
23
|
+
(U+3001) — right for zh/ja by accident, wrong in English and every Latin
|
|
24
|
+
locale. Latin packs use `, `, CJK `、`, Arabic `، `.
|
|
25
|
+
- The Spanish `validation.required` / `validation.unique` templates gained
|
|
26
|
+
their own masculine head noun (`El campo {{field}} es obligatorio`) so the
|
|
27
|
+
adjective agrees for feminine field labels too — `Cuenta es obligatorio` was
|
|
28
|
+
ungrammatical.
|
|
29
|
+
|
|
30
|
+
- 56409c2: Field widgets are finally told when their field fails validation, and the props
|
|
31
|
+
slot that carries it takes the name the published contract gives it
|
|
32
|
+
(objectui#3222).
|
|
33
|
+
|
|
34
|
+
**Breaking** for anyone implementing a field widget (see migration below). The
|
|
35
|
+
repo version policy keeps this a `minor` — objectui's major tracks
|
|
36
|
+
`@objectstack`'s — so read the bump as "breaking within objectui".
|
|
37
|
+
|
|
38
|
+
## The a11y defect this fixes
|
|
39
|
+
|
|
40
|
+
`@objectstack/spec/ui`'s `FieldWidgetPropsSchema` — the published contract that
|
|
41
|
+
third-party and AI-authored field widgets are written against — has always
|
|
42
|
+
declared `error?: string`. `@object-ui/fields` declared its own slot as
|
|
43
|
+
`errorMessage`. That looked like a naming split; it was worse:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
producers of `errorMessage` anywhere in packages/ + apps/ : 0
|
|
47
|
+
reads of `errorMessage` in packages/fields/src : 15 (7 widgets)
|
|
48
|
+
reads of `props.error` : 0
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The slot was dead under BOTH spellings. No host ever passed it: the form
|
|
52
|
+
renderer showed validation text through its own `<FormMessage/>` and never
|
|
53
|
+
forwarded the prop. So `EmailField`, `CurrencyField`, `UrlField`,
|
|
54
|
+
`RichTextField`, `PercentField`, `TextAreaField` and `PhoneField` each computed
|
|
55
|
+
`aria-invalid={!!errorMessage}` from a value that was `undefined` forever —
|
|
56
|
+
**`aria-invalid` had never once been set, and a screen reader was never told
|
|
57
|
+
the field had failed validation.**
|
|
58
|
+
|
|
59
|
+
Worse than "never set": `<FormControl>` is a Radix `Slot` that hands its child a
|
|
60
|
+
CORRECT `aria-invalid`, but a widget's own attribute is written after the props
|
|
61
|
+
spread, so it wins. Those seven widgets were actively overwriting the right
|
|
62
|
+
answer with `false`.
|
|
63
|
+
|
|
64
|
+
FROM: `renderFieldComponent` received no validation state, and the widget props
|
|
65
|
+
type declared `errorMessage?: string`, which nothing produced.
|
|
66
|
+
TO: the form renderer passes react-hook-form's `fieldState.error?.message` down
|
|
67
|
+
as `error` when it renders a registered widget, and the props type declares
|
|
68
|
+
`error?: string`. Both ends of the contract are live for the first time; a
|
|
69
|
+
rename alone would only have swapped one dead key for another.
|
|
70
|
+
|
|
71
|
+
## Migration for widget authors
|
|
72
|
+
|
|
73
|
+
```diff
|
|
74
|
+
-export function MyField({ value, onChange, field, readonly, errorMessage }: FieldWidgetComponentProps< string >) {
|
|
75
|
+
- return <Input value={value} aria-invalid={!!errorMessage} />;
|
|
76
|
+
+export function MyField({ value, onChange, field, readonly, error }: FieldWidgetComponentProps< string >) {
|
|
77
|
+
+ return <Input value={value} aria-invalid={!!error} />;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
No alias is kept. `errorMessage` was retained nowhere on purpose — a tolerant
|
|
81
|
+
second spelling is exactly the de-facto second contract AGENTS.md #0.1 forbids,
|
|
82
|
+
and it is what would let a missed call site go quiet again. Because
|
|
83
|
+
objectui#3221 had already removed the type's `[key: string]: any`, every missed
|
|
84
|
+
site is a compile error rather than a silent `any`, so the compiler — not grep
|
|
85
|
+
— validated this rename.
|
|
86
|
+
|
|
87
|
+
## Responsibilities are split, not duplicated
|
|
88
|
+
|
|
89
|
+
The widget consumes `error` **only** to drive `aria-invalid` on the control it
|
|
90
|
+
renders (which only it can do — `aria-invalid` has to sit on the input element).
|
|
91
|
+
The message TEXT stays with `<FormMessage/>` in the form renderer. A widget that
|
|
92
|
+
also renders the text double-displays it, and the docs, the agent prompt and the
|
|
93
|
+
tests all now say so.
|
|
94
|
+
|
|
95
|
+
For the same reason `required` — also declared by the spec, also never delivered
|
|
96
|
+
— is deliberately NOT lowered into widget props: the required marker has exactly
|
|
97
|
+
one author, the renderer's `<FormLabel>`, and giving widgets the flag invites a
|
|
98
|
+
second asterisk. The a11y state a widget could legitimately carry is
|
|
99
|
+
`aria-required`, which needs no contract change at all (`AriaAttributes` is
|
|
100
|
+
already part of the type and widgets already forward it).
|
|
101
|
+
|
|
102
|
+
Builtin field types are unaffected: they render inside `<FormControl>`, whose
|
|
103
|
+
Slot already supplies `aria-invalid`, so `error` is stripped there rather than
|
|
104
|
+
leaking into the DOM as a stray attribute.
|
|
105
|
+
|
|
106
|
+
Docs updated to match: `content/docs/guide/plugin-development.md`,
|
|
107
|
+
`skills/objectui/guides/plugin-development.md` and
|
|
108
|
+
`.github/prompts/component.prompt.md` — the last of which additionally used the
|
|
109
|
+
spec's non-generic type alias as a generic (`FieldWidgetProps< number >`) and
|
|
110
|
+
destructured a `mode` prop that exists on neither type.
|
|
111
|
+
|
|
112
|
+
- 042e09d: **BREAKING (v17)** — field widgets receive their metadata on ONE key, `field`.
|
|
113
|
+
`schema` is removed from the widget contract (objectui#3233).
|
|
114
|
+
|
|
115
|
+
## What changed
|
|
116
|
+
|
|
117
|
+
`schema` was a second carrier for what `field` already means. Two producers fed
|
|
118
|
+
it: `SchemaRenderer` passed the authored node as `schema`, and the form
|
|
119
|
+
renderer's `renderFieldComponent` passed `schema={props.field || props.schema ||
|
|
120
|
+
props}` _alongside_ `field`. The predictable result was ~30 widgets resolving
|
|
121
|
+
their config as `field || schema` — one concept, two spellings, a de-facto
|
|
122
|
+
second contract (AGENTS.md #0.1).
|
|
123
|
+
|
|
124
|
+
- `FieldWidgetComponentProps` no longer declares `schema`. Reading
|
|
125
|
+
`props.schema` is now a compile error, not a silent `any`.
|
|
126
|
+
- Both producers converged. The form renderer passes `field` only. The SDUI node
|
|
127
|
+
→ `field` translation happens exactly once, in a new registration adapter
|
|
128
|
+
(`withFieldCarrier`), which every built-in field widget is registered through.
|
|
129
|
+
- All `field || schema` reads in `@object-ui/fields` are now plain `field` reads.
|
|
130
|
+
|
|
131
|
+
## Migrating a widget you wrote
|
|
132
|
+
|
|
133
|
+
**Reading the metadata** — replace the fallback with the single key:
|
|
134
|
+
|
|
135
|
+
```diff
|
|
136
|
+
-const config = field || (props as any).schema;
|
|
137
|
+
+const config = field;
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Registering a widget** — if your widget can be rendered from a schema node
|
|
141
|
+
(anything `SchemaRenderer` dispatches, not just forms), wrap it once so it still
|
|
142
|
+
gets `field`:
|
|
143
|
+
|
|
144
|
+
```diff
|
|
145
|
+
+import { withFieldCarrier } from '@object-ui/fields';
|
|
146
|
+
+
|
|
147
|
+
-ComponentRegistry.register('color', ColorField, { namespace: 'field' });
|
|
148
|
+
+ComponentRegistry.register('color', withFieldCarrier(ColorField), { namespace: 'field' });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`withFieldCarrier` forwards the node **by reference** — nothing is copied,
|
|
152
|
+
narrowed or renamed — and consumes `schema` so it cannot reach the DOM through a
|
|
153
|
+
widget's `...props` spread.
|
|
154
|
+
|
|
155
|
+
A third-party widget that still reads `props.schema` and is **not** re-registered
|
|
156
|
+
through the adapter will read `undefined` in v17 and silently render an empty /
|
|
157
|
+
default state. That is the deliberate cost of a major boundary: one contract
|
|
158
|
+
beats N dialects, and a widget that picks the wrong spelling should fail at
|
|
159
|
+
compile time rather than work under one host and not another.
|
|
160
|
+
|
|
161
|
+
## What did NOT change
|
|
162
|
+
- **Host metadata (SDUI JSON) is untouched.** No authored schema changes; this is
|
|
163
|
+
a change to how widgets are _written_, not to what apps declare.
|
|
164
|
+
- **`schema` is still the universal SDUI prop** every registered component
|
|
165
|
+
receives from `SchemaRenderer` (`element:*`, `page:*`, grids, reports). Only
|
|
166
|
+
the _field-widget_ contract retired it. In particular `renderFieldComponent`
|
|
167
|
+
still passes `schema` when a form field type resolves to a plain component
|
|
168
|
+
through the bare-name fallback (e.g. `type: 'text'` reaching the display text
|
|
169
|
+
widget) — that component's contract is the node, and dropping it there would
|
|
170
|
+
render `undefined.className`.
|
|
171
|
+
|
|
172
|
+
## Payload equivalence
|
|
173
|
+
|
|
174
|
+
Every path that used to deliver a payload through `schema` now delivers the
|
|
175
|
+
identical object through `field`, and both halves are pinned by tests asserting
|
|
176
|
+
**object identity**, not shape:
|
|
177
|
+
|
|
178
|
+
- form path — `packages/components/src/renderers/form/__tests__/form-field-carrier.test.tsx`
|
|
179
|
+
- SDUI path — `packages/fields/src/__tests__/field-carrier-sdui.test.tsx`
|
|
180
|
+
|
|
181
|
+
### Patch Changes
|
|
182
|
+
|
|
183
|
+
- 532cf8b: Deliver the required state to the control in the five renderers outside the object form that still painted it as an asterisk only (objectui#3299 — the same defect #3290/#3298 fixed in `form.tsx`).
|
|
184
|
+
|
|
185
|
+
Each site converges on the reference shape (`EmbeddableForm.tsx`): the control carries `aria-required={required || undefined}` and the asterisk is `aria-hidden="true"`, so assistive tech announces required once, as a state — instead of hearing a bare "asterisk" folded into the accessible name, or nothing at all.
|
|
186
|
+
|
|
187
|
+
- `@object-ui/app-shell` — `ActionParamDialog` (both the boolean row and the default branch, delivered through the real field widgets' `toDomProps` whitelist) and `CreateViewDialog` (display label, machine name, and every type-specific required-field selector).
|
|
188
|
+
- `@object-ui/components` — the custom `ActionParamDialog` (all five typed branches, including the Radix select trigger) and `FieldContainer`, whose existing Slot injection (`id` / `aria-describedby` / `aria-invalid`) now also injects `aria-required`, covering every consumer in one place.
|
|
189
|
+
- `@object-ui/plugin-detail` — `InlineCreateRelated`'s create-tab inputs.
|
|
190
|
+
|
|
191
|
+
Deliberately NOT the native `required` attribute (#3290 ruling): each of these hosts runs its own validation, and native `required` would arm the browser's constraint-validation bubble beside it. The SDUI controls that already use native `required` (`renderers/form/{input,textarea,select,checkbox}.tsx`, `basic/text-input.tsx`) are unchanged — they don't have a second validator, so their channel is already correct.
|
|
192
|
+
|
|
193
|
+
- 680080a: The required state now reaches the input control as `aria-required`, instead of
|
|
194
|
+
existing only as part of the control's accessible name (objectui#3290).
|
|
195
|
+
|
|
196
|
+
The form renderer has always computed a correct `required` — the static
|
|
197
|
+
`required` flag merged with the `requiredWhen` CEL verdict — and then spent it
|
|
198
|
+
on exactly one thing: the red asterisk in `<FormLabel>`. That asterisk carried
|
|
199
|
+
`aria-label="required"`, so the only path from the computed state to assistive
|
|
200
|
+
tech was `<label for>` folding it into the control's **accessible name**: the
|
|
201
|
+
field was announced as "Title required".
|
|
202
|
+
|
|
203
|
+
A state smuggled through a name is broken three ways:
|
|
204
|
+
|
|
205
|
+
- it is read in name order rather than announced as a state, and "list the
|
|
206
|
+
required fields" style navigation cannot see it at all;
|
|
207
|
+
- a field rendered without a `label` (compact layouts, inline grid editing)
|
|
208
|
+
draws no asterisk, so the signal disappears entirely;
|
|
209
|
+
- `requiredWhen` makes required **dynamic**, and a state channel can express
|
|
210
|
+
the flip where a name cannot.
|
|
211
|
+
|
|
212
|
+
## What changed
|
|
213
|
+
- Every field control — built-in (`input` / `textarea` / `checkbox` / `switch` /
|
|
214
|
+
`select`) and registered widget alike — now receives `aria-required="true"`
|
|
215
|
+
when the field resolves required, and **no attribute at all** when it does
|
|
216
|
+
not. Absence rather than `aria-required="false"` is deliberate: it is what
|
|
217
|
+
the `requiredWhen`-turns-false case has to produce.
|
|
218
|
+
- The red asterisk is now `aria-hidden="true"` and no longer carries
|
|
219
|
+
`aria-label="required"`. It renders exactly as before for sighted users; it
|
|
220
|
+
simply stops being announced, so the state is reported once (as a state) and
|
|
221
|
+
not twice.
|
|
222
|
+
|
|
223
|
+
**No field widget needed a change.** `aria-required` is already declared and
|
|
224
|
+
typed on the widget props contract (`FieldWidgetComponentProps &
|
|
225
|
+
AriaAttributes`) and every widget forwards its leftover props to the control it
|
|
226
|
+
renders, so all 48 widgets pick it up unmodified.
|
|
227
|
+
|
|
228
|
+
Two non-changes, both deliberate:
|
|
229
|
+
|
|
230
|
+
- **No native `required` attribute.** That would arm the browser's own
|
|
231
|
+
constraint-validation bubble alongside react-hook-form's `<FormMessage/>` —
|
|
232
|
+
two validators, two UIs, one field. `aria-required` reports the state without
|
|
233
|
+
triggering native validation.
|
|
234
|
+
- **No `required` boolean in the widget props contract.** A boolean would give
|
|
235
|
+
the required marker a second author, and the next widget draws its own
|
|
236
|
+
asterisk next to the renderer's — the double-display failure objectui#3222
|
|
237
|
+
already declined for the validation message.
|
|
238
|
+
|
|
239
|
+
If you select the asterisk in a test, it is now
|
|
240
|
+
`span[data-required-marker]` — an explicit locator, rather than an
|
|
241
|
+
accessibility attribute doubling as a test hook.
|
|
242
|
+
|
|
243
|
+
- a7651e6: The form renderer's built-in `select` branch stops saying "No options available"
|
|
244
|
+
in English to non-English sessions (objectui#3263).
|
|
245
|
+
|
|
246
|
+
FROM: the inline branch that renders a `type: 'select'` field — the one taken
|
|
247
|
+
whenever the field is a `BUILTIN_FIELD_TYPES` member, i.e. before the `field:`
|
|
248
|
+
registry is consulted — rendered `{emptyHint || 'No options available'}`. TO:
|
|
249
|
+
`{emptyHint || t('fields.options.empty')}`, the same i18n key the registered
|
|
250
|
+
option widgets fall back to (objectui#3231, all ten locale packs).
|
|
251
|
+
|
|
252
|
+
This was the last hardcoded copy of that sentence in `form.tsx`, and the file was
|
|
253
|
+
half-translated in a way a user could see inside one widget: the dependency-gate
|
|
254
|
+
sentence next to it already went through `t()`
|
|
255
|
+
(`fields.options.selectFirst`), so under a `zh` session a gated select read
|
|
256
|
+
"请先选择Country" and the same select — one keystroke later, when the parent
|
|
257
|
+
value matched no option — flipped to English.
|
|
258
|
+
|
|
259
|
+
`fields.options.empty` is added to `useSafeFormTranslation`'s defaults map, the
|
|
260
|
+
pattern `fields.options.selectFirst` already follows there, so a form rendered
|
|
261
|
+
with no `I18nProvider` (standalone widget, test, embedded form) produces the
|
|
262
|
+
byte-identical English string it produced before. Both halves are pinned by
|
|
263
|
+
tests: the Chinese rendering in one file, the no-provider English fallback in
|
|
264
|
+
another (mounting a provider installs it as react-i18next's global default,
|
|
265
|
+
which would erase the state the second one observes).
|
|
266
|
+
|
|
267
|
+
The box moved from an inline `<div>` into a small `BuiltinSelectEmptyState`
|
|
268
|
+
component in the same file, because `renderFieldComponent` is a plain helper that
|
|
269
|
+
early-returns on the registered-widget path — a hook called there would run
|
|
270
|
+
conditionally. It forwards its rest props, since `<FormControl>` is a Radix
|
|
271
|
+
`Slot` that supplies the control's `id` / `aria-describedby`; a test pins that
|
|
272
|
+
the field's `<label for>` still resolves to this box.
|
|
273
|
+
|
|
274
|
+
Deliberately NOT unified with `@object-ui/fields`' `OptionsEmptyState`: different
|
|
275
|
+
package, different render path (inline branch vs. registry). What the two share
|
|
276
|
+
is the i18n key, not a component — merging them would impose one path's markup
|
|
277
|
+
and props on the other.
|
|
278
|
+
|
|
279
|
+
- b71fc92: Localize the last untranslated console-chrome accessible names (objectstack#5430)
|
|
280
|
+
|
|
281
|
+
Four icon-only controls still carried hardcoded English accessible names, so
|
|
282
|
+
under a non-English session they were the only English left in the record
|
|
283
|
+
chrome — and because the controls have no visible label, that literal _is_ the
|
|
284
|
+
control to a screen reader and to the hover tooltip.
|
|
285
|
+
|
|
286
|
+
- `page:header`'s `role="toolbar"` — now `detail.pageHeaderActions` (its `⋯`
|
|
287
|
+
overflow trigger eight lines below was fixed in #5407; the toolbar was missed)
|
|
288
|
+
- `ReactionPicker`'s `role="listbox"` popup — now `detail.emojiPicker`
|
|
289
|
+
- `ReactionPicker`'s per-reaction chip, which built its name by concatenation
|
|
290
|
+
with English pluralization baked in (`reaction${count !== 1 ? 's' : ''}`) —
|
|
291
|
+
now `detail.reactionCount` / `detail.reactionCountOne`
|
|
292
|
+
- `NavigationOverlay`'s drawer close and split-panel close — now `common.close`
|
|
293
|
+
(the key the rest of the console already uses) and `common.closePanel`
|
|
294
|
+
|
|
295
|
+
The pluralized label follows this repo's **two-key** convention
|
|
296
|
+
(`detail.relatedRecords`/`relatedRecordOne`, `lookup.recordCount`/`recordCountOne`)
|
|
297
|
+
rather than an i18next `_one`/`_other` pair: zh/ja/ko have no separate singular
|
|
298
|
+
form, so those packs would legitimately omit the `_one` half and
|
|
299
|
+
`all-locales-key-parity` would read that as a lost key.
|
|
300
|
+
|
|
301
|
+
All five new keys are added to all ten locale packs.
|
|
302
|
+
|
|
303
|
+
- 34595eb: The Combobox trigger now declares `type="button"` explicitly, so it can never
|
|
304
|
+
submit an enclosing `<form>` (objectui#3344). The current Radix
|
|
305
|
+
`PopoverTrigger` happens to supply `type="button"` through its Slot, but that
|
|
306
|
+
form-safety guarantee was an upstream implementation detail — it is now a
|
|
307
|
+
locally declared, regression-tested contract, matching the explicit style of
|
|
308
|
+
LookupField / MultiSelectField / RatingField. The default sits before the
|
|
309
|
+
trigger pass-through spread (objectui#3318), so a consumer who explicitly
|
|
310
|
+
passes `type` (e.g. `type="submit"`) still wins.
|
|
311
|
+
- 9cbcbf4: The form renderer's built-in `textarea` branch reads the fullscreen long-text
|
|
312
|
+
flag on one spelling (objectui#3303).
|
|
313
|
+
|
|
314
|
+
FROM: the branch resolved the affordance as `mobile_fullscreen || fullscreen`,
|
|
315
|
+
and both prop strips (`stripRendererOnlyProps`, `stripRegisteredFieldProps`)
|
|
316
|
+
carried a matching entry discarding a `fullscreen` key. TO: a single read of
|
|
317
|
+
`mobile_fullscreen`, with no strip entry left for the alias.
|
|
318
|
+
|
|
319
|
+
No runtime behaviour changes for anything that exists, because the second term
|
|
320
|
+
was permanently `undefined`. `fullscreen` had **no producer**: a repo-wide grep
|
|
321
|
+
plus `objectstack`'s `packages/spec` turns up only the unrelated
|
|
322
|
+
feedback/loading overlay property of the same name, never a form field. The one
|
|
323
|
+
real producer is `ObjectForm`, which stamps `mobile_fullscreen` onto long-text
|
|
324
|
+
fields from `ObjectFormSchema.mobile.fullscreenLongText` — the same single
|
|
325
|
+
carrier `TextAreaField` and `RichTextField` read.
|
|
326
|
+
|
|
327
|
+
This closes the last member of the convergence run objectui#3232 / #3233 /
|
|
328
|
+
#3245 / #3301 started. The changeset for #3232 named this branch explicitly as
|
|
329
|
+
"a separate live path" still accepting two spellings; it is now single-read like
|
|
330
|
+
the two widgets, so the same authored metadata behaves the same way whether a
|
|
331
|
+
field type resolves to a registered widget or falls through to the built-in
|
|
332
|
+
branch.
|
|
333
|
+
|
|
334
|
+
Why a no-producer alias is worth removing rather than leaving as harmless
|
|
335
|
+
insurance: it is not insurance, it is a contract that never held. The renderer
|
|
336
|
+
advertised a spelling to whoever reads it next — very much including an AI
|
|
337
|
+
writing form metadata — and that spelling silently does nothing, with no error
|
|
338
|
+
and nowhere to look. That is the lenient consumer fallback AGENTS.md #0.1
|
|
339
|
+
forbids, and the identical mechanism behind #3245 and #3301. Dropping the strip
|
|
340
|
+
entries matters for the same reason: a key nobody produces should not get a
|
|
341
|
+
dedicated discard, it should be in the ordinary unrecognised-key class, so a
|
|
342
|
+
typo is as visible as any other typo instead of being quietly swallowed.
|
|
343
|
+
|
|
344
|
+
Pinned by tests in both places the alias lived: the built-in branch renders the
|
|
345
|
+
expand affordance for `mobile_fullscreen` and not for `fullscreen` (the
|
|
346
|
+
canonical case is asserted alongside the alias case, so the negative cannot pass
|
|
347
|
+
for the empty reason of the affordance having disappeared altogether), and the
|
|
348
|
+
strips are shown to own `mobile_fullscreen` — stripped from the top-level props,
|
|
349
|
+
delivered on `field` — while `fullscreen` is now indistinguishable from an
|
|
350
|
+
arbitrary unknown key.
|
|
351
|
+
|
|
352
|
+
- 85c4c9c: 表单内置 `textarea` 的全屏编辑对话框现在能拿到字段自己的 label:对话框标题显示字段名而不是恒定的通用词「编辑文本」,同一张表单上多个长文本字段的展开按钮也终于有了互不相同的无障碍名(objectui#3393)。
|
|
353
|
+
|
|
354
|
+
`renderFormField` 在解构字段配置时把 `label` 单独取走了(它要渲染 `<FormLabel>`),而下游 `renderFieldComponent` 唯一调用点重建 props 对象时显式补回了 `field` / `inputType` / `options` / `placeholder` / `emptyHint` / `dependsOnLabels` 等等,唯独漏了 `label`。于是内置 `textarea` 分支里的 `label` 恒为 `undefined`,`FullscreenTextarea` 中两条依赖它的分支从写下那天起就没走到过:
|
|
355
|
+
|
|
356
|
+
- 对话框标题 `label ?? t('form.fullscreen.title')` 永远落到通用词——一个叫「备注」的字段点开全屏编辑,标题不会说自己是「备注」;
|
|
357
|
+
- 展开按钮的无障碍名永远插值通用名词,一张有三个长文本字段的表单上三个按钮读屏完全一样。读屏用户无法判断自己要展开的是哪个字段,这是可达性缺陷而不是观感问题。
|
|
358
|
+
|
|
359
|
+
## 改了什么
|
|
360
|
+
- 调用点显式转发 `label`(与 `placeholder` / `emptyHint` 同法),这是唯一的行为改动。
|
|
361
|
+
- `label` 属于 renderer-only:`stripRendererOnlyProps` 与 `stripRegisteredFieldProps` 各加一条丢弃项,所以它既不会变成 DOM 上的 `label="备注"` 杂属性(每个内置分支都会把剩余 props 直接摊到 DOM 节点上),也不会成为注册型 widget 新收到的 prop——自 v17 起 `field` 是它们唯一的元数据载体(objectui#3233),label 一直在那里读。
|
|
362
|
+
- 内置 `textarea` 分支里那句 `const { label: _label, ...rest }` 随之删除。它本想拦住 label 落到 DOM,但既然从来没有 label 送进来,它拦的是不存在的东西(ESLint 一直报着 `'_label' is assigned a value but never used`),而且只护住了这一个分支。现在这件事由 strip 统一负责,所有分支同等受护。
|
|
363
|
+
|
|
364
|
+
十个语言包零改动:#3272 把 `form.fullscreen.toggle` 做成了带 `{{label}}` 插值的整句(zh 插在句尾、ja 插在句首),label 一通,十个语言的句子直接就对。字段没有 label 时仍回落到被翻译的通用词。
|
|
365
|
+
|
|
366
|
+
- fd54c3e: The form renderer's built-in `textarea` branch now honours `readonly` / `disabled` on its fullscreen exit, which previously bypassed both (objectui#3400). `renderFieldComponent` destructures `readonly` off its props, so it is absent from the `...rest` each branch spreads; the plain exit put it back as `readOnly={readonly}` plus the read-only tint, the `mobile_fullscreen` exit put back neither. `FullscreenTextarea` then renders three controls — the inline textarea, the expand button, and the dialog's own textarea — of which only the first ever saw a spread prop. So a read-only long-text field was editable in place without even opening the dialog, and a disabled one looked correctly greyed out while its expand button stayed live, its dialog accepted any edit, and "Done" wrote that edit back into form state. Neither is an exotic combination: `ObjectForm` stamps `mobile_fullscreen` onto every long-text field when `mobile.fullscreenLongText` is set without consulting either flag, `readonly` is also resolved at runtime from a `readonlyWhen` CEL rule, and `disabled` is additionally true for the whole form while a submit is in flight — so a submit in progress did not stop a long-text edit either. A read-only field now renders no expand button at all, matching the registered `TextAreaField` path so both renderers give the same metadata the same behaviour; a disabled field keeps the button but disables it. Both states also lock the dialog's textarea and its "Done" button independently, because `disabled` can flip to true while the dialog is already open. Editable long-text fields are unaffected.
|
|
367
|
+
- 4eeb932: The form renderer's last user-visible English literals now go through i18n (#3272). The fullscreen long-text editor (`mobile_fullscreen`) was an entire untranslated dialog — title, screen-reader description, `Cancel` / `Done` footer buttons, and the expand trigger's accessible name — rendering English inside an otherwise translated zh/ja/ar form; it now reads the new `form.fullscreen.*` keys, shipped in all ten locale packs.
|
|
368
|
+
|
|
369
|
+
**Behaviour change worth reading if you author forms:** `submitLabel` and `cancelLabel` no longer default to the literals `'Submit'` and `'Cancel'` in the renderer. They default to _unset_, and the action bar falls back at render time to `common.submit` / `common.cancel`, so a form that declares no button copy now follows the session language instead of being silently frozen to English. A label you DO declare still wins verbatim in every locale — including an English one under a zh session, and including an explicit empty string (the fallback uses `??`, so `submitLabel: ''` renders a blank button rather than being overwritten). The only forms whose rendered text changes are those that never declared the labels and are viewed in a non-English session — which is the bug. `FormSchema.submitLabel` / `cancelLabel` stay optional strings; no spec or type change.
|
|
370
|
+
|
|
371
|
+
Also removed the built-in `select` branch's second `|| 'Select an option'` fallback. The single call site already supplies `t('common.selectOption')`, so the literal was reachable only through an authored `placeholder: ''` — where it replaced the author's deliberate blank with an untranslated English word.
|
|
372
|
+
|
|
373
|
+
- 53811d1: Associate the label with its control at the two form surfaces where the two were never programmatically connected (objectui#3341 — found while implementing #3299/PR #3340, and deliberately left out of that PR's scope fence as a different class of defect).
|
|
374
|
+
|
|
375
|
+
`aria-required` reaching the control (#3299) only fixes the required _state_; at these two sites the control's accessible _name_ was still wrong, because the label pointed at nothing:
|
|
376
|
+
|
|
377
|
+
- `@object-ui/plugin-detail` — `InlineCreateRelated`'s create-tab fields rendered a `<label>` with no `htmlFor` beside an `<Input>` with no `id`, and the two were siblings rather than wrapper/child. The field label was unreachable for assistive tech, and clicking the label did not focus the input. The ids are namespaced with `React.useId`, because `field.name` alone is unique only within one instance and a detail page mounts one of these per related list.
|
|
378
|
+
- `@object-ui/components` — the custom `ActionParamDialog`'s `select` branch rendered `<Label htmlFor={param.name}>` but never put the matching `id` on its Radix `SelectTrigger`, so the reference dangled. The textarea / number / date / text branches already set `id={param.name}`; select was the only one that did not.
|
|
379
|
+
|
|
380
|
+
`SelectTrigger` renders a `<button role="combobox">`, and `button` is a labelable element, so the plain `htmlFor`/`id` pair is the correct association there — no `aria-labelledby` required. No spec change and no widget-props contract change.
|
|
381
|
+
|
|
382
|
+
- d915c47: Relation fields (`lookup` / `master_detail` / `user` / `tree`) are now usable in action and conditional-formatting predicates: they bind as the stored foreign key on every surface, and the fields a predicate reads are included in the query projection (#3501).
|
|
383
|
+
|
|
384
|
+
Before this, one predicate over one relation field had four different fates, decided by things its author does not control. `$expand` **replaces** the id in place with the whole related record, and a view expands exactly the relations it shows as COLUMNS — so `record.owner == "U1"` was **true** where the column was absent, **false** where it was displayed, and a **fault** where the field was neither displayed nor projected (a list's `$select` was built from its columns alone, and CEL treats an absent key as a fault, not as null). A fault is fail-CLOSED on the row kebab and the selection bar and fail-OPEN on the lenient paths, so the same authoring mistake hid the button from everyone on one surface and showed it to everyone on the next, with nothing on screen to point at either. The server, meanwhile, only ever sees the id — so client and server could not agree, which is the one thing ADR-0036 / ADR-0058 exist to guarantee.
|
|
385
|
+
|
|
386
|
+
Two changes close it. `toPredicateRecord` (new, `@object-ui/core`) collapses expanded relation values back to their ids when a record is bound for evaluation — driven by the object's own field types, not by sniffing for an `id` key, so a `json` field that happens to carry one is untouched. It is threaded through `evalRowPredicate` / `resolveConditionalFormatting` (via a new `fields` option), `useRowPredicate`, `partitionBulkRows`, and both `page:header` evaluators, with the object schema supplied by `ObjectGrid` / `ListView` / `ObjectKanban` / the record context. Kanban card formatting is threaded the same way, so a rule cannot match on the grid view of a list and silently never match on its board. Display is unaffected — a detail-page title still renders the related record's name, and the schema-only `kanban-ui` entry point (which has no object schema to offer) keeps using the payload verbatim. `collectPredicateFieldRefs` / `listViewPredicates` (new) harvest the `record.x` / `data.x` references out of a view's conditional formatting, row-action defs, bulk-action defs, promoted object actions and `userActions` overrides, and add them to `$select` — intersected with the object's declared fields plus the platform columns every object carries (`isProjectableField`), because an unknown key is not ignored by every backend. No `$expand` is added: a predicate wants the foreign key, which is what an unexpanded relation already is.
|
|
387
|
+
|
|
388
|
+
- 825bbe3: The option widgets' "this list cannot be filled" message now has one source, and
|
|
389
|
+
it is translated (objectui#3231).
|
|
390
|
+
|
|
391
|
+
FROM: `SelectField`, `MultiSelectField`, `RadioField` and `CheckboxesField` each
|
|
392
|
+
carried their own copy of the empty/gated state, each destructured the declared
|
|
393
|
+
`emptyHint` prop into `_emptyHint` and dropped it, and each rendered a hardcoded
|
|
394
|
+
English literal (`'No options available'`, `` `Select ${…} first` ``) even in a
|
|
395
|
+
Chinese or Japanese session. TO: one shared `OptionsEmptyState` — the host's
|
|
396
|
+
`emptyHint` when it supplied one, otherwise a translated fallback
|
|
397
|
+
(`fields.options.empty` / `fields.options.selectFirst`, added to all ten locale
|
|
398
|
+
packs).
|
|
399
|
+
|
|
400
|
+
`emptyHint` was declared, produced by the form renderer and transported, then
|
|
401
|
+
lost three times over — so no registered widget could ever render it. All three
|
|
402
|
+
breaks are fixed, because closing only the last one delivers nothing:
|
|
403
|
+
|
|
404
|
+
- `isOptionField` compared the raw resolved type against `'select'` /`'radio'` /
|
|
405
|
+
`'multiselect'` / `'checkboxes'`. Object-derived forms emit
|
|
406
|
+
`mapFieldTypeToFormType`'s prefixed ids (`field:select`), which matched none of
|
|
407
|
+
them, so for every option field coming from an object schema — the normal case
|
|
408
|
+
in the console — the whole cascade block was skipped and no hint was computed
|
|
409
|
+
at all. It now normalizes the `field:` prefix, the same normalization
|
|
410
|
+
`stripRegisteredFieldProps` already applied a few lines below.
|
|
411
|
+
- `stripRegisteredFieldProps` then removed the `emptyHint` key from what was
|
|
412
|
+
left. It is now forwarded to the four cascade option types, alongside
|
|
413
|
+
`dependentValues`. This stays an allow-list rather than a blanket
|
|
414
|
+
pass-through: every other registered widget spreads its leftover props onto a
|
|
415
|
+
DOM node, where an unknown `emptyHint` attribute is a React warning.
|
|
416
|
+
- the widgets themselves discarded it. Keeping it out of the `...props` spread
|
|
417
|
+
was correct; not using it afterwards was not.
|
|
418
|
+
|
|
419
|
+
User-visible effect: a dependency-gated option list now prompts with the
|
|
420
|
+
controlling field's **label** ("Select Country first") instead of its raw
|
|
421
|
+
metadata name, in the session's language; an unconfigured list says so in the
|
|
422
|
+
session's language too. The gate sentence is one i18n key shared by the renderer
|
|
423
|
+
and the widget fallback, so the two sides cannot word it differently.
|
|
424
|
+
|
|
425
|
+
Untouched: the built-in (unregistered) `select` branch of the form renderer,
|
|
426
|
+
which already consumed `emptyHint`. That is a separate live path.
|
|
427
|
+
|
|
428
|
+
- 5dd0127: Localize the record-overlay and tab-badge chrome that #5430's sweep left behind (objectstack#5506)
|
|
429
|
+
|
|
430
|
+
Four more console-chrome strings were still hardcoded English literals. Unlike
|
|
431
|
+
#5430's set they are not all accessible names — one is visible copy, and one was
|
|
432
|
+
a component **default** that only the console happened to override.
|
|
433
|
+
|
|
434
|
+
- `page:tabs`' count badge built its `aria-label` by template literal,
|
|
435
|
+
`` `${formatTabCount(count)} items` ``. The badge renders digits only, so that
|
|
436
|
+
label _is_ the badge to a screen reader — and the English plural was baked in
|
|
437
|
+
with no singular branch at all, so a related list with one row announced
|
|
438
|
+
"1 items". Now `common.itemCount` / `common.itemCountOne`.
|
|
439
|
+
- `NavigationOverlay`'s drag-resize handle (`role="separator"`, no visible label)
|
|
440
|
+
— now `common.resizeDrawer`.
|
|
441
|
+
- `NavigationOverlay`'s `expandLabel` **default**. Hosts may override it and the
|
|
442
|
+
console does, but the default is what every other host ships — and it feeds
|
|
443
|
+
both `aria-label` and `title` of an icon-only button. Now
|
|
444
|
+
`detail.openAsFullPage`, still overridable by the prop.
|
|
445
|
+
- `NavigationOverlay`'s `resolvedTitle` fallback, `'Record Detail'` — **visible**
|
|
446
|
+
overlay heading, not just an a11y name. Now `detail.recordDetail`.
|
|
447
|
+
- The sr-only `SheetDescription`/`DialogDescription` prose
|
|
448
|
+
`Record detail overlay for {title}.`, which existed in three copies
|
|
449
|
+
(drawer / modal / popover) — now one `detail.recordDetailOverlay` key with a
|
|
450
|
+
`{{title}}` placeholder.
|
|
451
|
+
|
|
452
|
+
The count badge follows this repo's **two-key** plural convention
|
|
453
|
+
(`detail.reactionCount`/`reactionCountOne`, `detail.relatedRecords`/`relatedRecordOne`)
|
|
454
|
+
rather than an i18next `_one`/`_other` pair: zh/ja/ko have no separate singular
|
|
455
|
+
form, so those packs would legitimately omit the `_one` half and
|
|
456
|
+
`all-locales-key-parity` would read that as a lost key. The formatted count
|
|
457
|
+
(`1.2k`, not `1200`) is interpolated so the accessible name and the visible
|
|
458
|
+
digits never disagree — and because i18next skips its own plural resolution when
|
|
459
|
+
`count` is a string, the two-key scheme stays in charge of the choice.
|
|
460
|
+
|
|
461
|
+
Both touched components moved from `useSafeTranslate` to `createSafeTranslation`,
|
|
462
|
+
which carries an options bag (two of the new keys interpolate) and an English
|
|
463
|
+
defaults map. That map is what keeps the provider-less path English, which
|
|
464
|
+
consumers outside this package depend on — `plugin-view`'s `ObjectView.test.tsx`
|
|
465
|
+
and `e2e/live/inline-edit-polish-2572.spec.ts` address this chrome by English
|
|
466
|
+
accessible name with no `I18nProvider` mounted.
|
|
467
|
+
|
|
468
|
+
All six new keys are added to all ten locale packs.
|
|
469
|
+
|
|
470
|
+
- 06632e9: `PageRenderer` no longer renders its own `<h1>` when the page authors a titled `page:header`, so a page has exactly one level-1 heading. Every non-record page used to render the page `title`/`label` as an `h1` _and_ let the `page:header` block render a second one — on the showcase master-detail page both said "New Project + Tasks", producing a broken document outline, a page title a screen reader announces twice, and the same string printed twice on screen. Record pages already delegated the whole title block to `page:header`; that rule now holds for `app` / `home` / `utility` pages too, and it is what the live e2e was reporting as a Playwright strict-mode violation (`getByRole('heading', { name })` resolving to 2 elements, objectui#3434).
|
|
471
|
+
|
|
472
|
+
Delegation is deliberately conservative: only a `page:header` whose title renders literal text takes the heading over. A header with no title — or one whose title interpolates to nothing (e.g. `title: '{name}'` with no record in scope) — renders no heading of its own, so the page keeps its implicit `h1` rather than ending up with none. The page-level `description` is unaffected; it is the page's own prose, not a duplicate of the header `subtitle`.
|
|
473
|
+
|
|
474
|
+
Author-visible effect: on a page carrying both a `label` and a titled `page:header`, only the header's title is shown (e.g. app-crm's welcome page shows "Welcome to the CRM", not also "CRM Welcome"). Pages without a `page:header` are unchanged.
|
|
475
|
+
|
|
476
|
+
- a4cff5b: Conditional-rule predicates that fail to evaluate are no longer silent
|
|
477
|
+
(objectstack#5149, appeal 2). `evalFieldPredicate` — the canonical funnel for
|
|
478
|
+
`visibleWhen` / `readonlyWhen` / `requiredWhen`, view-level `visibleOn`, legacy
|
|
479
|
+
`condition`, per-option `visibleWhen`, screen-field predicates and list
|
|
480
|
+
conditional formatting — now logs **one `console.warn` per predicate text**
|
|
481
|
+
when evaluation fails (parse error, unbound identifier, engine fault), carrying
|
|
482
|
+
the predicate source, the engine's failure reason, and the field/rule locator
|
|
483
|
+
the call site provides. Renderer call sites thread that locator
|
|
484
|
+
(`visibleWhen of field 'amount'`), so a broken predicate identifies itself in
|
|
485
|
+
the browser console instead of being indistinguishable from an absent one.
|
|
486
|
+
|
|
487
|
+
Verdicts are unchanged: evaluation still fails open to the caller's safe
|
|
488
|
+
default (flipping that default is objectstack#5149 appeal 1, tracked
|
|
489
|
+
separately). Fault-probing callers (`evalRowPredicate`'s fail-closed path,
|
|
490
|
+
`ExpressionEvaluator`'s `throwOnError`) opt out via the new
|
|
491
|
+
`diagnostic.warn: false` and keep their own single diagnostic, so no broken
|
|
492
|
+
predicate ever warns twice.
|
|
493
|
+
|
|
494
|
+
- 71be406: The close button that the `Sheet` and `Dialog` primitives auto-render now announces itself in the session locale instead of always in English. Both buttons are icon-only (a lucide `X`), so their `sr-only` span is not decoration — it IS the control's accessible name, and upstream Shadcn ships it as a hardcoded English literal. Under zh/ja/es every drawer and modal in the console (~20 `SheetContent` consumers plus every `DialogContent` consumer — ChatDock, ActivityFeed, metadata-admin, AiChatPage, BuildDebugDrawer, PeoplePicker, RecordDetailDrawer, …) announced "Close" to a screen reader. The span now renders `<CloseSrLabel />`, which resolves `common.close` (present in all ten locale packs since objectstack#5430) and falls back to English when no `I18nProvider` is mounted, so existing suites and e2e specs that address these controls by their English name are unaffected (objectstack#5505).
|
|
495
|
+
|
|
496
|
+
Because `packages/components/src/ui/**` is regenerated from the Shadcn registry, the edit is not a hand patch that the next `pnpm shadcn:update` would silently revert: it is declared as data in `scripts/shadcn-local-patches.mjs`, re-applied automatically on every sync (including `--force`), and enforced in both directions — `pnpm shadcn:check` now exits non-zero if a declared patch is missing from the file on disk or can no longer be re-applied to current upstream, and an offline test gates the same invariant on every PR.
|
|
497
|
+
|
|
498
|
+
- 8d8094a: 20 more registered field widgets now announce a failed validation to assistive
|
|
499
|
+
tech: `multiselect`, `radio`, `checkboxes`, `tags`, `lookup`, `master_detail`,
|
|
500
|
+
`user`, `owner`, `file`, `image`, `location`, `object`, `color`, `rating`,
|
|
501
|
+
`code`, `avatar`, `address`, `geolocation`, `qrcode` and `object-ref` carry
|
|
502
|
+
`aria-invalid="true"` on their real focusable control after a validation
|
|
503
|
+
failure, where before the red message rendered while a screen reader was told
|
|
504
|
+
nothing (objectui#3318, the registry-wide gap objectui#3306's sweep measured).
|
|
505
|
+
|
|
506
|
+
Each widget follows the objectui#3222/#3306 pattern: the `toDomProps(props)`
|
|
507
|
+
whitelist spread goes onto the control the user actually focuses — the input,
|
|
508
|
+
the lookup trigger button, the radiogroup (`role="radiogroup"` is the
|
|
509
|
+
ARIA-designated carrier for a set of radios), every chip/checkbox/star of the
|
|
510
|
+
composite option widgets, the upload dropzone/button — followed by an explicit
|
|
511
|
+
`aria-invalid={!!error}` computed from the published `error` slot. Wrapper
|
|
512
|
+
`<div>`s never carry the state, and `name` is withheld from non-form-control
|
|
513
|
+
elements (the objectui#3291 leak class).
|
|
514
|
+
|
|
515
|
+
`Combobox` (`@object-ui/components`) now accepts standard button attributes
|
|
516
|
+
and forwards them to its focusable `role="combobox"` trigger, giving
|
|
517
|
+
combobox-based widgets an element to deliver `aria-invalid` /
|
|
518
|
+
`aria-describedby` to — the same seam objectui#3306 opened on
|
|
519
|
+
`SelectTrigger`.
|
|
520
|
+
|
|
521
|
+
Nine types remain on the objectui#3318 ratchet ledger with their blockers
|
|
522
|
+
documented there (`formula`/`summary`/`auto_number`/`vector` render no
|
|
523
|
+
focusable control; `grid`, `slider`, `signature` need component-level design;
|
|
524
|
+
`filter-condition`/`recipient-picker` deliver in their editable states but
|
|
525
|
+
render a dependency-gate hint with no control in a fresh form).
|
|
526
|
+
|
|
527
|
+
- Updated dependencies [18cd432]
|
|
528
|
+
- Updated dependencies [d915c47]
|
|
529
|
+
- Updated dependencies [b71fc92]
|
|
530
|
+
- Updated dependencies [65516ba]
|
|
531
|
+
- Updated dependencies [94c5b7c]
|
|
532
|
+
- Updated dependencies [ca0fa8f]
|
|
533
|
+
- Updated dependencies [3889ffb]
|
|
534
|
+
- Updated dependencies [5781fb1]
|
|
535
|
+
- Updated dependencies [7e2406a]
|
|
536
|
+
- Updated dependencies [9e9e9a9]
|
|
537
|
+
- Updated dependencies [4eeb932]
|
|
538
|
+
- Updated dependencies [5c856ec]
|
|
539
|
+
- Updated dependencies [23018cc]
|
|
540
|
+
- Updated dependencies [68b6a28]
|
|
541
|
+
- Updated dependencies [0554e88]
|
|
542
|
+
- Updated dependencies [d915c47]
|
|
543
|
+
- Updated dependencies [f44d872]
|
|
544
|
+
- Updated dependencies [28b2e65]
|
|
545
|
+
- Updated dependencies [509104a]
|
|
546
|
+
- Updated dependencies [825bbe3]
|
|
547
|
+
- Updated dependencies [6195841]
|
|
548
|
+
- Updated dependencies [5dd0127]
|
|
549
|
+
- Updated dependencies [a415684]
|
|
550
|
+
- Updated dependencies [a4cff5b]
|
|
551
|
+
- Updated dependencies [175bd79]
|
|
552
|
+
- Updated dependencies [5af2852]
|
|
553
|
+
- Updated dependencies [f833d3a]
|
|
554
|
+
- Updated dependencies [a6ec93d]
|
|
555
|
+
- Updated dependencies [2a9513d]
|
|
556
|
+
- Updated dependencies [d22ae31]
|
|
557
|
+
- Updated dependencies [c7ed4c3]
|
|
558
|
+
- Updated dependencies [2409e1d]
|
|
559
|
+
- Updated dependencies [789fe3e]
|
|
560
|
+
- @object-ui/core@17.3.0
|
|
561
|
+
- @object-ui/types@17.3.0
|
|
562
|
+
- @object-ui/i18n@17.3.0
|
|
563
|
+
- @object-ui/react@17.3.0
|
|
564
|
+
- @object-ui/sdui-parser@17.3.0
|
|
565
|
+
- @object-ui/react-runtime@17.3.0
|
|
566
|
+
|
|
567
|
+
## 17.2.0
|
|
568
|
+
|
|
569
|
+
### Minor Changes
|
|
570
|
+
|
|
571
|
+
- 4ae0ac4: One placement rule for action `locations` (objectui#3142).
|
|
572
|
+
|
|
573
|
+
**Breaking for metadata**: an action that declares no `locations` (missing key
|
|
574
|
+
or `[]`) no longer renders in a located surface. FROM: omitting `locations`
|
|
575
|
+
made an action appear on the list toolbar, the record header, and every
|
|
576
|
+
metadata-admin toolbar. TO: declare where it belongs —
|
|
577
|
+
`locations: ['record_header']` for the record header, `['list_toolbar']` for
|
|
578
|
+
the list toolbar, and so on. Nothing else changes; actions that already
|
|
579
|
+
declare a location are untouched.
|
|
580
|
+
|
|
581
|
+
Four renderers each answered "where does an action with no `locations` go?"
|
|
582
|
+
differently — `action:bar` and metadata-admin showed it EVERYWHERE,
|
|
583
|
+
`page:header` showed it on the header, `action:group` showed it for
|
|
584
|
+
`undefined` but hid it for `[]` — while `ActionEngine`, `RecordDetailView`,
|
|
585
|
+
`DeclaredActionsBar`, the related-list bridge and the environment toolbar all
|
|
586
|
+
showed it NOWHERE. The same action therefore appeared or vanished depending on
|
|
587
|
+
which component happened to draw it. All eight now go through one exported
|
|
588
|
+
predicate, `actionRendersAt(action, location)` from `@object-ui/types`: an
|
|
589
|
+
action renders at a location only if it declares that location.
|
|
590
|
+
|
|
591
|
+
The strict reading is the platform's own — ADR-0078 lists "an `action` with no
|
|
592
|
+
`locations`" as a verified inert shape, and the detail-page synthesizer already
|
|
593
|
+
documented "must include `locations: ['record_header']` to render". The
|
|
594
|
+
leniency contradicted both, and it is what let an aggregate-only bulk action
|
|
595
|
+
(objectui#3139) — one with no single-record placement by construction — mint a
|
|
596
|
+
list-toolbar button whose dispatch could only fail.
|
|
597
|
+
|
|
598
|
+
Two placements are declared elsewhere and need no `locations`, both unchanged:
|
|
599
|
+
host-injected chrome in the `systemActions` / `headerSystemActions` slot (now
|
|
600
|
+
consistently exempt on `page:header` too, where it used to be filtered), and an
|
|
601
|
+
action named in a view's `bulkActions` / `bulkActionDefs`.
|
|
602
|
+
|
|
603
|
+
Authoring side: Studio seeds `locations: ['record_header']` on a new action
|
|
604
|
+
instead of minting one that renders nowhere, and the action inspector says so
|
|
605
|
+
when no placement is ticked. The `ActionSchema.locations` JSDoc claimed a
|
|
606
|
+
`['record_header']` default that no renderer ever implemented — corrected.
|
|
607
|
+
|
|
608
|
+
- 09d30a4: Stop declaring 18 `@object-ui/auth` / `@object-ui/components` / `@object-ui/react`
|
|
609
|
+
symbols under names `@objectstack/spec` owns (objectui#3159, objectstack#4115
|
|
610
|
+
batch 5).
|
|
611
|
+
|
|
612
|
+
**Breaking for importers of all three packages** — six exported names changed,
|
|
613
|
+
because the spec exports the same name for a _different_ thing:
|
|
614
|
+
|
|
615
|
+
| package | was | now | what the spec's same-named export actually is |
|
|
616
|
+
| :----------- | :--------------------------- | :----------------------- | :----------------------------------------------------------------------------- |
|
|
617
|
+
| `auth` | `AuthSession` | `AuthClientSession` | the SERVER's session record (`{ id, userId, expiresAt: ISO string, token? }`) |
|
|
618
|
+
| `auth` | `AuthProviderConfig` | `AuthProviderOptions` | an OAuth/OIDC provider registration (`{ id, clientId, clientSecret, scope? }`) |
|
|
619
|
+
| `components` | `FilterCondition` | `FilterBuilderCondition` | the recursive ObjectQL predicate AST (`$and`/`$or`/`$not`) |
|
|
620
|
+
| `components` | `Field` | `FieldContainer` | an object FIELD's metadata and its builder namespace |
|
|
621
|
+
| `react` | `ConflictResolutionStrategy` | `ConflictResolution` | the metadata-MERGE policy (`error \| priority \| first-wins \| last-wins`) |
|
|
622
|
+
|
|
623
|
+
The `react` rename is the odd one out: the new name is the **spec's own** name
|
|
624
|
+
for the union that hook always used, so it is a re-export rather than a dialect.
|
|
625
|
+
|
|
626
|
+
Eleven more keep their names and are now **imported or derived from the spec**
|
|
627
|
+
instead of re-declared: `TenancyPosture`, `DelegableScope` (+`DelegableAdminScope`),
|
|
628
|
+
`AuthUser`, `ShareLinkPermission`, `ShareLinkAudience`, `ShareLink`, `SortItem`,
|
|
629
|
+
`OfflineStrategy`, `OfflineCacheConfig`, `OfflineSyncConfig`, `OfflineConfig`,
|
|
630
|
+
`NavigationConfig`.
|
|
631
|
+
|
|
632
|
+
**Three of the copies were losing information, not just duplicating it.**
|
|
633
|
+
|
|
634
|
+
- `AuthUser` never declared the spec's `positions` or `tenantId` — the
|
|
635
|
+
authorization inputs. Its `[key: string]: unknown` index signature meant the
|
|
636
|
+
omission was invisible at every call site _and_ to any structural comparison
|
|
637
|
+
(the objectstack#4075 mechanism). It now `extends` the spec principal, so the
|
|
638
|
+
display-only fields (`image`, `role`, `roles`, `emailVerified`) are the delta
|
|
639
|
+
and the spec's keys arrive on their own.
|
|
640
|
+
- `useNavigationOverlay`'s copy carried the note _"inline … to avoid importing
|
|
641
|
+
from `@object-ui/types` (which may not be a direct dependency of
|
|
642
|
+
`@object-ui/react`)"_. The vocabulary belongs to `@objectstack/spec`, which
|
|
643
|
+
**is** a direct dependency — the same expired "kept local to avoid a
|
|
644
|
+
dependency" comment objectui#3169 found in `@object-ui/app-shell`.
|
|
645
|
+
- `useOffline` and `usePerformance` both opened with _"Types aligned with
|
|
646
|
+
`@objectstack/spec` v2.0.7"_. The installed spec is 17.0.0-rc.1.
|
|
647
|
+
|
|
648
|
+
`ShareLink` derives from the spec row **minus `password_hash`** — omitted rather
|
|
649
|
+
than optional, because it is the credential itself and typing it in a browser
|
|
650
|
+
package is an invitation to render it. `password_protected` (the boolean the UI
|
|
651
|
+
needs in its place) is the one local addition.
|
|
652
|
+
|
|
653
|
+
The config types derive from each schema's **input** side, not `z.infer`.
|
|
654
|
+
`useOffline(config: OfflineConfig = {})` defaults to the empty object, which the
|
|
655
|
+
output type — every `.default()`ed key required — would reject outright.
|
|
656
|
+
|
|
657
|
+
`@objectstack/spec` moves from `devDependencies` to `dependencies` in
|
|
658
|
+
`@object-ui/components`: its public type surface now references the spec.
|
|
659
|
+
|
|
660
|
+
Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
|
|
661
|
+
tracks `@objectstack`, so breaking changes of our own ship as minor with the
|
|
662
|
+
semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
|
|
663
|
+
all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.
|
|
664
|
+
|
|
665
|
+
- 726b89c: `@object-ui/types` stops declaring sixteen symbols under names `@objectstack/spec` owns (objectui#3156, objectstack#4115).
|
|
666
|
+
|
|
667
|
+
Seven are now **derived** from the spec, nine are **renamed** to the local
|
|
668
|
+
dialect they always were. Both halves remove the same hazard: a local
|
|
669
|
+
declaration under a spec export's name reads as the spec's own definition to
|
|
670
|
+
the next reader, so a copy that is merely _correct today_ is a planted premise
|
|
671
|
+
tomorrow.
|
|
672
|
+
|
|
673
|
+
**Derived** — the spec now supplies the keys, by reference:
|
|
674
|
+
|
|
675
|
+
| symbol | derivation |
|
|
676
|
+
| :----------------------- | :-------------------------------------------------------------------------------- |
|
|
677
|
+
| `ActionParam` | `z.input<typeof ActionParamSchema>`, `type` widened to the local legacy spellings |
|
|
678
|
+
| `CreateExportJobRequest` | `Omit<CreateExportJobInput, 'object'>` (`object` is the method argument) |
|
|
679
|
+
| `CreateExportJobResult` | re-export from `@objectstack/spec/contracts` |
|
|
680
|
+
| `ImportRowResult` | re-export from `@objectstack/spec/api` |
|
|
681
|
+
| `NavigationArea` | spec keys, with `navigation` / `visible` pinned locally |
|
|
682
|
+
| `NavigationAreaSchema` | `specFieldsExcept(NavigationAreaSchema.shape, …)` |
|
|
683
|
+
| `Theme` | re-export of the spec's `ThemeInput` (the authoring shape) |
|
|
684
|
+
| `ExportJobFormat` | re-export of the spec's `ExportFormat` |
|
|
685
|
+
|
|
686
|
+
Four of these close real gaps rather than tidy names. `ActionParam` never
|
|
687
|
+
declared `reference` — the key `resolveActionParams()` actually reads for an
|
|
688
|
+
inline lookup target — nor `defaultFromRow`, which the metadata designer's own
|
|
689
|
+
inspector writes; it also narrowed `visible` to a bare string although the
|
|
690
|
+
resolver has always accepted the `{ dialect, source }` envelope too.
|
|
691
|
+
`CreateExportJobResult.createdAt` and `ImportRowResult.action` were optional
|
|
692
|
+
here and required by the server, leaving every consumer a branch that could
|
|
693
|
+
never run. And `NavigationArea`'s `id` now carries the spec's own length rule
|
|
694
|
+
instead of accepting any string.
|
|
695
|
+
|
|
696
|
+
**Renamed** — same word, different concept:
|
|
697
|
+
|
|
698
|
+
| was | now | why |
|
|
699
|
+
| :----------------- | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
|
|
700
|
+
| `FileMetadata` | `UploadedFileMetadata` | field-VALUE payload (`url`, `original_name`), not the storage file record |
|
|
701
|
+
| `GestureType` | `TouchGestureType` | direction-fused (`swipe-left`), not the spec's type+direction pair |
|
|
702
|
+
| `GestureConfig` | `TouchGestureConfig` | gesture→`action` binding, not per-gesture tuning |
|
|
703
|
+
| `OfflineConfig` | `PWAOfflineConfig` | service-worker route caching, not the offline data/sync model |
|
|
704
|
+
| `PageRegion` | `PageNodeRegion` | region of the renderer page NODE, holding `SchemaNode`s |
|
|
705
|
+
| `PageRegionSchema` | `PageNodeRegionSchema` | zod twin of the above |
|
|
706
|
+
| `ResponsiveConfig` | `MobileResponsiveConfig` | mobile box config, not the spec's SDUI grid contract |
|
|
707
|
+
| `WidgetManifest` | `RuntimeWidgetManifest` | SDUI component manifest, not the field-widget plugin manifest |
|
|
708
|
+
| `WidgetSource` | `RuntimeWidgetSource` | `module`/`inline`/`registry` loader union — and its `inline` carries a resolved component where the spec's carries source code |
|
|
709
|
+
|
|
710
|
+
**Migration**: the old names are gone, not deprecated — an alias would preserve
|
|
711
|
+
exactly the ambiguity being removed. Import the new name; nothing about the
|
|
712
|
+
shapes changed. `@object-ui/types` already re-exports the spec's own
|
|
713
|
+
`SpecResponsiveConfig`, and `@object-ui/react`'s `useOffline` config remains the
|
|
714
|
+
spec-shaped `OfflineConfig`, so both concepts stay reachable under
|
|
715
|
+
distinguishable names.
|
|
716
|
+
|
|
717
|
+
Each rename carries a bidirectional tripwire
|
|
718
|
+
(`packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts`): it fails if
|
|
719
|
+
the spec ever claims the new name, and also if the spec retires the old one —
|
|
720
|
+
at which point the natural name can be taken back rather than the workaround
|
|
721
|
+
outliving its reason.
|
|
722
|
+
|
|
723
|
+
### Patch Changes
|
|
724
|
+
|
|
725
|
+
- cb82705: A standalone grid's search box searches the list, not the page you can see (objectui#3118).
|
|
726
|
+
|
|
727
|
+
Under server-side pagination a standalone `ObjectGrid` rendered `data-table`'s
|
|
728
|
+
built-in search box, and that box filtered the rows the table was holding —
|
|
729
|
+
which is one page. The user read "2 results for X in this list" while 3075 rows
|
|
730
|
+
never participated, with the pager beside it still reading `1 / 63`. Every piece
|
|
731
|
+
was individually correct: `searchable` defaults to true, `manualPagination` is
|
|
732
|
+
true, and the two are declared next to each other in the same object literal.
|
|
733
|
+
|
|
734
|
+
This is objectui#3106 one axis over — sort there, filter here — and it takes the
|
|
735
|
+
same shape. `DataTable` gains `manualSearch` + a controlled `search` +
|
|
736
|
+
`onSearchChange`. In that mode it filters nothing, reports the typed term, and
|
|
737
|
+
renders `search` as the box's value, holding **no** term of its own: a private
|
|
738
|
+
copy beside a controlled prop is the shape the defect had. `ObjectGrid` turns
|
|
739
|
+
that term into a `$search` on the refetch — the server picks the matching fields
|
|
740
|
+
from the object's metadata (ADR-0061), the same channel the ListView toolbar has
|
|
741
|
+
always used — and returns to page 1, since a new term makes the old page index a
|
|
742
|
+
different set of rows (usually no rows at all). `$searchFields` rides along only
|
|
743
|
+
when the view declared `searchableFields`, which can narrow the server-resolved
|
|
744
|
+
set and never widen it.
|
|
745
|
+
|
|
746
|
+
Two things worth naming:
|
|
747
|
+
|
|
748
|
+
- Both paths are never live at once. The server's answer is the answer; a client
|
|
749
|
+
pass left running underneath would silently re-narrow it to whichever returned
|
|
750
|
+
rows happen to contain the term as _rendered text_, overruling the server's
|
|
751
|
+
own notion of which fields are searchable.
|
|
752
|
+
- Under `manualSearch` a table with no `onSearchChange` renders **no** search
|
|
753
|
+
box. The sort axis could degrade to inert headers; here there is no honest
|
|
754
|
+
local behaviour to fall back to, because the rows to search are not in the
|
|
755
|
+
browser.
|
|
756
|
+
|
|
757
|
+
Client-paginated grids are untouched: inline, bound and grouped grids hold every
|
|
758
|
+
row they display, so their box keeps filtering in memory, where the count it
|
|
759
|
+
produces is true. The ListView path was never affected — it passes
|
|
760
|
+
`showSearch: false` and searches from its own toolbar.
|
|
761
|
+
|
|
762
|
+
- f6e8d78: Lookup search inside a create/edit modal is typeable again (objectui#3183).
|
|
763
|
+
|
|
764
|
+
In every production console build, the search input of a lookup field's
|
|
765
|
+
quick-select popover — and the nested Record Picker dialog — could not take
|
|
766
|
+
focus while the form modal was open: every click/focus was synchronously
|
|
767
|
+
yanked back to the field trigger, so a lookup could not be searched while
|
|
768
|
+
creating a record.
|
|
769
|
+
|
|
770
|
+
Root cause is a race in stock `@radix-ui/react-focus-scope@1.1.16`: the
|
|
771
|
+
focus-scopes stack effect's cleanup schedules `focusScopesStack.remove(scope)`
|
|
772
|
+
in a `setTimeout(0)`. When the effect re-runs for a still-mounted scope (a
|
|
773
|
+
`container` ref flicker), the re-run re-`add`s the scope and the stale timeout
|
|
774
|
+
then evicts it — the dialog's trap listeners stay active but its scope is no
|
|
775
|
+
longer in the stack, so an opening popover pauses nothing and the trap yanks
|
|
776
|
+
focus out of the popover forever.
|
|
777
|
+
|
|
778
|
+
Fixed via `patches/@radix-ui__react-focus-scope.patch`: an effect re-run for a
|
|
779
|
+
live scope cancels the pending eviction; a real unmount still runs the full
|
|
780
|
+
delayed cleanup (autofocus-on-unmount + stack removal). Regression-tested in
|
|
781
|
+
`packages/components` with a deterministic reproduction of the race.
|
|
782
|
+
|
|
783
|
+
- a8ad6c0: A required boolean must be savable in its UNCHECKED state — `false` and `0` are values.
|
|
784
|
+
|
|
785
|
+
Reported against an AI-built task tracker whose 任务 object has a required
|
|
786
|
+
`是否完成` boolean: the create form showed the switch OFF, answered "是否完成不能
|
|
787
|
+
为空", and saved instantly once the switch was turned ON. The app could only ever
|
|
788
|
+
create ALREADY-DONE tasks — the one state the control shows by default was the
|
|
789
|
+
one value it refused to save (cloud#972).
|
|
790
|
+
|
|
791
|
+
Two defects stacked, and either alone is enough to break it:
|
|
792
|
+
|
|
793
|
+
**The `required` verdict read truthiness, not presence.** `@objectstack/spec`
|
|
794
|
+
FieldSchema.required (ADR-0113) is "an insert must provide a NON-NULL value",
|
|
795
|
+
and objectql's record validator implements exactly that. react-hook-form's
|
|
796
|
+
built-in rule instead fails whenever `isBoolean(value) && !value` — its
|
|
797
|
+
accept-the-terms checkbox heritage — silently redefining every required boolean
|
|
798
|
+
as "must be TRUE", including a select whose chosen option value is `false`. It
|
|
799
|
+
also disagreed the other way, letting a whitespace-only string through for the
|
|
800
|
+
server to reject with a 400. The form renderer no longer hands RHF its own
|
|
801
|
+
`required`: the check is now a `validate` entry keyed `required` (so the error
|
|
802
|
+
still surfaces as `type: 'required'`, which the conditional-required cleanup
|
|
803
|
+
keys on) backed by a new shared `isMissingForRequired` in `@object-ui/core`, a
|
|
804
|
+
deliberate mirror of objectql `record-validator.isMissing` — `undefined`,
|
|
805
|
+
`null`, blank-after-trim string, empty array. Deleting the inherited rule also
|
|
806
|
+
stops a `required` that rode in on `validation` from outliving a `requiredWhen`
|
|
807
|
+
that resolved to FALSE.
|
|
808
|
+
|
|
809
|
+
**A boolean field held `undefined` while displaying "off".** A two-state control
|
|
810
|
+
has no third state, but a field with no entry in `defaultValues` rendered an OFF
|
|
811
|
+
switch backed by nothing: the create payload omitted the column (it lands null,
|
|
812
|
+
which reads as unchecked but isn't) and the presence check above would still
|
|
813
|
+
refuse it. The form renderer now folds `false` into `defaultValues` for every
|
|
814
|
+
boolean-widget field the caller left unset — in `defaultValues` itself, not
|
|
815
|
+
per-Controller, because that object is also the dirty-check baseline and what
|
|
816
|
+
the defaults-reset window replays. Every surface gets it, including the
|
|
817
|
+
modal/drawer create dialogs that start from a bare `{}`. An authored default
|
|
818
|
+
(or a loaded record, `null` included) still wins.
|
|
819
|
+
|
|
820
|
+
`WizardForm`'s cross-step gate had its own copy of the empty-value predicate; it
|
|
821
|
+
now imports the shared one so it cannot drift from the per-field verdict. And
|
|
822
|
+
the field-demo renderer read `schema.defaultValue || schema.value`, throwing
|
|
823
|
+
away an authored default of `false` / `0` / `''` — same falsy-as-empty class,
|
|
824
|
+
now `??`.
|
|
825
|
+
|
|
826
|
+
Verified end to end on a local stack against the exact metadata shape
|
|
827
|
+
`apply_blueprint` materializes (`{ type: 'boolean', required: true }`, no
|
|
828
|
+
default): a 是否完成 = 否 task with 工时 = 0 now creates and persists as
|
|
829
|
+
`{ hours: 0, is_done: false }`, turning the switch on still stores `true`, and a
|
|
830
|
+
blank required text is still refused.
|
|
831
|
+
|
|
832
|
+
- Updated dependencies [4ae0ac4]
|
|
833
|
+
- Updated dependencies [696e3c1]
|
|
834
|
+
- Updated dependencies [bca45cc]
|
|
835
|
+
- Updated dependencies [a889e31]
|
|
836
|
+
- Updated dependencies [09d30a4]
|
|
837
|
+
- Updated dependencies [4bf612c]
|
|
838
|
+
- Updated dependencies [335041c]
|
|
839
|
+
- Updated dependencies [b414983]
|
|
840
|
+
- Updated dependencies [256f8cc]
|
|
841
|
+
- Updated dependencies [d9668a7]
|
|
842
|
+
- Updated dependencies [4b470b9]
|
|
843
|
+
- Updated dependencies [cb82705]
|
|
844
|
+
- Updated dependencies [f572849]
|
|
845
|
+
- Updated dependencies [4a51e77]
|
|
846
|
+
- Updated dependencies [ea96284]
|
|
847
|
+
- Updated dependencies [d3584c6]
|
|
848
|
+
- Updated dependencies [cc70b8f]
|
|
849
|
+
- Updated dependencies [a8ad6c0]
|
|
850
|
+
- Updated dependencies [444457c]
|
|
851
|
+
- Updated dependencies [850033c]
|
|
852
|
+
- Updated dependencies [022e4c3]
|
|
853
|
+
- Updated dependencies [009e25d]
|
|
854
|
+
- Updated dependencies [726b89c]
|
|
855
|
+
- @object-ui/types@17.2.0
|
|
856
|
+
- @object-ui/core@17.2.0
|
|
857
|
+
- @object-ui/react@17.2.0
|
|
858
|
+
- @object-ui/i18n@17.2.0
|
|
859
|
+
- @object-ui/sdui-parser@17.2.0
|
|
860
|
+
- @object-ui/react-runtime@17.2.0
|
|
861
|
+
|
|
3
862
|
## 17.1.0
|
|
4
863
|
|
|
5
864
|
### Minor Changes
|