@object-ui/plugin-form 17.3.0 → 17.5.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 CHANGED
@@ -1,5 +1,504 @@
1
1
  # @object-ui/plugin-form
2
2
 
3
+ ## 17.5.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 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
8
+
9
+ 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.
10
+
11
+ **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.
12
+
13
+ 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.
14
+
15
+ **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.
16
+
17
+ 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.
18
+
19
+ 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.
20
+
21
+ - c32a8a1: `richtext` fields are placed like the long-form fields they are — four layout sets stopped spelling the type three ways the spec rejects
22
+
23
+ `@objectstack/spec` spells the WYSIWYG type `richtext`, one word, and **rejects** `rich_text` and `rich-text`: both exist only as typo keys in the spec's own `suggestFieldType` table, so `FieldSchema` refuses a field declared with either. Four sets that place fields by matching the RAW type string carried nothing else — `SKIP_TYPES` in the related list spelled it `rich_text`, both `WIDE_FIELD_TYPES` and `SECONDARY_FIELD_TYPES` spelled it `rich-text` — so each set was inert for the only spelling a producer can emit, and every one of them named the type it was failing to handle.
24
+
25
+ For a real `richtext` field that meant: it was auto-derived into a related-list column, it never spanned the full row in a multi-column detail section or form (unlike `markdown` and `html` sitting right beside it in the same sets), and it stayed in the dense primary section of the record page instead of dropping into "More details". All four move together — half of them would have left the detail page and the form disagreeing about the same field, which is worse than the uniform gap.
26
+
27
+ The dead spellings are dropped rather than kept alongside the live one: the alias table is the single place aliases belong, and a set that carries both invites the next drift. The pins are derived from the spec's own `FieldType` vocabulary instead of enumerated, so a member that stops being a real type name fails by name — replacing an assertion that was green only because the set contained the string it asked about.
28
+
29
+ `markdown` joins `richtext` and `html` in the related list's `SKIP_TYPES`, on a measurement rather than on the assumption that it renders raw. It does not: markdown and richtext both render through `MarkdownCellRenderer`, formatted and sanitized. The reason none of the three works in a table is that the formatted output is block-level — a heading, paragraphs, a list — inside a single-line truncating cell, so a document shows as one clipped heading with the rest invisible. `textarea` stays derived for the same reason read the other way: it renders as plain truncated text, which is a useful column. Author-declared columns are untouched — this set only filters the zero-config auto-derive walk.
30
+
31
+ - Updated dependencies [0e67b53]
32
+ - Updated dependencies [ceccdcf]
33
+ - Updated dependencies [d6e5124]
34
+ - Updated dependencies [debad27]
35
+ - Updated dependencies [dc2aa3e]
36
+ - Updated dependencies [ee66e2e]
37
+ - Updated dependencies [e2e6360]
38
+ - Updated dependencies [ee26e65]
39
+ - Updated dependencies [5900ac5]
40
+ - Updated dependencies [932cbcd]
41
+ - Updated dependencies [734d186]
42
+ - Updated dependencies [f650253]
43
+ - Updated dependencies [3d9769a]
44
+ - Updated dependencies [8f85f8b]
45
+ - Updated dependencies [d0c3b26]
46
+ - Updated dependencies [3fc2971]
47
+ - Updated dependencies [aca27fa]
48
+ - Updated dependencies [dde7283]
49
+ - Updated dependencies [f7c6430]
50
+ - Updated dependencies [4dadf0d]
51
+ - Updated dependencies [ae10a01]
52
+ - Updated dependencies [0f21348]
53
+ - Updated dependencies [d2e2caf]
54
+ - Updated dependencies [92876f0]
55
+ - Updated dependencies [f279deb]
56
+ - Updated dependencies [4b70d28]
57
+ - Updated dependencies [eb7f586]
58
+ - Updated dependencies [e901131]
59
+ - Updated dependencies [ebb4e0e]
60
+ - Updated dependencies [3a9021e]
61
+ - Updated dependencies [d9d3463]
62
+ - Updated dependencies [2a40f69]
63
+ - Updated dependencies [bec3e14]
64
+ - Updated dependencies [613b167]
65
+ - Updated dependencies [b4d3c22]
66
+ - Updated dependencies [1f9b905]
67
+ - Updated dependencies [8f60d73]
68
+ - Updated dependencies [cb13400]
69
+ - Updated dependencies [828549a]
70
+ - Updated dependencies [e1ade8f]
71
+ - Updated dependencies [bc64bfe]
72
+ - Updated dependencies [abb0f81]
73
+ - Updated dependencies [38ab505]
74
+ - Updated dependencies [3e19fe7]
75
+ - Updated dependencies [bb58d1d]
76
+ - Updated dependencies [433ff9f]
77
+ - Updated dependencies [5cc847c]
78
+ - Updated dependencies [e7663f2]
79
+ - Updated dependencies [fa21254]
80
+ - Updated dependencies [33c32bf]
81
+ - Updated dependencies [66fb4fa]
82
+ - Updated dependencies [b953a97]
83
+ - Updated dependencies [d7f3e30]
84
+ - Updated dependencies [6d641c9]
85
+ - Updated dependencies [7e4f0e5]
86
+ - Updated dependencies [a84385b]
87
+ - Updated dependencies [45e1949]
88
+ - Updated dependencies [92250d6]
89
+ - Updated dependencies [c1d939f]
90
+ - Updated dependencies [58bebf6]
91
+ - Updated dependencies [36310dc]
92
+ - Updated dependencies [52d878a]
93
+ - Updated dependencies [405e808]
94
+ - Updated dependencies [49ae9f4]
95
+ - Updated dependencies [a3ae404]
96
+ - Updated dependencies [bfdf3d4]
97
+ - Updated dependencies [bb68488]
98
+ - Updated dependencies [c0f9a4b]
99
+ - Updated dependencies [b1e42d0]
100
+ - Updated dependencies [2459a3e]
101
+ - Updated dependencies [ac853ce]
102
+ - Updated dependencies [fa51109]
103
+ - Updated dependencies [d6aa172]
104
+ - Updated dependencies [fe52a04]
105
+ - Updated dependencies [d46f9b8]
106
+ - Updated dependencies [3f5f87c]
107
+ - Updated dependencies [2fea4d2]
108
+ - Updated dependencies [f5e1143]
109
+ - Updated dependencies [7f1cb33]
110
+ - Updated dependencies [f148a64]
111
+ - Updated dependencies [bb68488]
112
+ - Updated dependencies [2e3b0c0]
113
+ - Updated dependencies [9461dd3]
114
+ - Updated dependencies [78fa331]
115
+ - Updated dependencies [47f551b]
116
+ - Updated dependencies [31ab1ac]
117
+ - Updated dependencies [0082db8]
118
+ - Updated dependencies [ab04728]
119
+ - Updated dependencies [5bf09fd]
120
+ - Updated dependencies [06915b0]
121
+ - Updated dependencies [ff84b05]
122
+ - @object-ui/i18n@17.5.0
123
+ - @object-ui/react@17.5.0
124
+ - @object-ui/components@17.5.0
125
+ - @object-ui/core@17.5.0
126
+ - @object-ui/fields@17.5.0
127
+ - @object-ui/types@17.5.0
128
+ - @object-ui/permissions@17.5.0
129
+
130
+ ## 17.4.0
131
+
132
+ ### Minor Changes
133
+
134
+ - 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)
135
+
136
+ `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.
137
+
138
+ 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.
139
+
140
+ Removed, with a tombstone at each site:
141
+
142
+ - `@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.
143
+ - `@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.
144
+ - `@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.
145
+ - `@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.
146
+
147
+ `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.
148
+
149
+ `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.
150
+
151
+ - 1bd6faa: fix(fields,plugin-form): stop the inline child grid from collapsing `datetime`/`time` columns onto the `date` control
152
+
153
+ `deriveMasterDetail`'s `fieldTypeToColumnType` mapped `date`, `datetime` and `time` onto the single `date` grid column type, and `GridField` renders that as `<input type="date">`. The consequence was not cosmetic: that control emits a bare `YYYY-MM-DD` on change, so a user who merely re-picked the **day** of a `datetime` cell silently wrote the record's time component out of existence — a `14:30` became midnight with no warning and no undo.
154
+
155
+ `GridColumn['type']` now carries `'datetime'` and `'time'` alongside `'date'`, and each renders its own control with its own read/write adapter:
156
+
157
+ - `datetime` → `<input type="datetime-local">`, read through `toDateTimeInputValue` and written back through `fromDateTimeInputValue`, so the stored shape stays ISO-8601 and read and write share one basis (the contract `DateTimeField` already follows, objectui#3127).
158
+ - `time` → `<input type="time">`, round-tripping the stored zone-less `HH:mm[:ss]` verbatim.
159
+ - `date` → unchanged.
160
+
161
+ The read-only surfaces are fixed with it. `displayText()` and the read-only table both fell through to `String(value)` and printed the raw stored ISO on screen (`2026-06-17T00:00:00.000Z`); each temporal type now formats as itself — a day for `date`, day + local time for `datetime`, the wall clock for `time`. That could not be fixed before the type collapse was undone, because with one column type the renderer had no way to know which of the two to show.
162
+
163
+ Authors writing explicit grid `columns` can now declare `type: 'datetime'` / `type: 'time'`; previously those spellings were not part of the exported union.
164
+
165
+ ### Patch Changes
166
+
167
+ - 8497579: A required field whose `defaultValue` is a runtime token is submittable from a create form
168
+
169
+ `@objectstack/spec` lets a field's `defaultValue` be a runtime _instruction_
170
+ rather than a value — the `DEFAULT_VALUE_TOKENS` family (`'NOW()'`,
171
+ `'current_user'`) or a CEL Expression envelope. The server resolves those per
172
+ insert, in `ObjectQL.applyFieldDefaults`, for any field that arrives absent or
173
+ null, which is why a create form must leave them empty: seeding the literal text
174
+ `NOW()` into a datetime input and submitting it suppresses the very resolution
175
+ the declaration asked for.
176
+
177
+ Correct for an optional field. Combined with `required: true` it deadlocked:
178
+
179
+ ```ts
180
+ remind_at: Field.datetime({ required: true, defaultValue: 'NOW()' }),
181
+ ```
182
+
183
+ the control opened empty, the client-side required rule refused the submit, and
184
+ there was nothing sensible for the user to type — the declaration had already
185
+ said what the value is, and omitting the field is exactly what makes the server
186
+ supply it. Same shape as the `required` + static-default case, one layer down.
187
+
188
+ In **create** mode a runtime `defaultValue` now suppresses the client-side
189
+ `required` rule, and the field is omitted from the payload. The producer
190
+ guarantees the value at insert, so the field is not "missing" — it is
191
+ server-owned. `required: true` alongside a runtime default is coherent authoring
192
+ (storage-level required, producer-guaranteed), not an authoring error.
193
+
194
+ Both halves matter. Suppressing the rule alone would have been half an answer: a
195
+ rendered control registers with the form whether or not anything seeded it, so
196
+ an untouched runtime-default field still reached the payload as `undefined` — or
197
+ as `''` once anything focused it. `undefined` is invisible to a
198
+ `JSON.stringify` inspection while remaining a KEY a data source may translate
199
+ into an explicit column write, and `''` is neither absent nor null, so it stores
200
+ a blank and defeats the declaration outright.
201
+
202
+ Three boundaries came with it, each pinned in both directions:
203
+
204
+ - **Create only.** An edit form shows a persisted row, where the token was
205
+ resolved at insert; blanking a required column there is a real removal and is
206
+ still refused.
207
+ - **Runtime defaults only.** A static literal default _is_ seeded into the
208
+ control, so if the user clears it they have removed a value that was really
209
+ there — `required` still fires.
210
+ - **The rule, not the field.** A value the user does type is submitted normally
211
+ and outranks the declared default. Only the "must not be empty" check is
212
+ suppressed.
213
+
214
+ Seeding and this rule read ONE predicate (`isRuntimeDefault`), so a form can
215
+ never seed a field it also refuses to submit. The suppression also drops the
216
+ required marker and `aria-required` for that field in create mode, since both
217
+ are driven by the same boolean — the honest reading, as the user really is not
218
+ required to provide the value. Surfacing what the server _will_ supply, as a
219
+ non-authoritative preview, is a separate follow-up.
220
+
221
+ Not extended to `requiredWhen` (the conditional-required CEL rule), which is
222
+ resolved downstream in the form renderer against the live record.
223
+
224
+ - f0c9a90: Create forms now open with the object schema's declared `defaultValue`s
225
+
226
+ A field declared `required: true, defaultValue: 'draft'` opened the console's
227
+ create dialog with an empty select and a required marker: the user had to pick a
228
+ value the system already knew, with every neighbouring option — some with side
229
+ effects — one click away. `defaultValue` + `required` produced the worst create
230
+ experience of any modelling choice, strictly worse than declaring no default.
231
+
232
+ The server was never the problem. Omitting the field from a create request
233
+ stores the declared default, because `ObjectQL.applyFieldDefaults` resolves it on
234
+ insert. The gap was container-side: `ObjectForm` seeded its opening values from
235
+ the object schema, and the five other object-form containers did not — their
236
+ create branch set the form data to `initialData || initialValues || {}` and never
237
+ looked at the schema. The console's create dialog is the global `<ModalForm>`,
238
+ one of those five. Modal, Drawer, Tabbed, Split and Wizard now seed through one
239
+ shared module (`schemaDefaults`), so a create form opens preselected and
240
+ submittable.
241
+
242
+ Three boundaries came with it, each pinned in both directions:
243
+
244
+ - **Create only.** An edit form shows a persisted row as the server holds it.
245
+ `ObjectForm`'s pass had been running in every mode, so a column the record
246
+ leaves unset showed the default — arming a silent write of a value the user
247
+ never chose on the next save of any other field. It is now gated on the same
248
+ "no persisted record" test the data-fetch effect uses.
249
+ - **Static defaults only.** A `defaultValue` may be an instruction the server
250
+ resolves per insert — the `NOW()` / `current_user` runtime tokens
251
+ (`DEFAULT_VALUE_TOKENS`) or a CEL Expression envelope. `ObjectForm` had been
252
+ seeding those verbatim, which put the literal text `NOW()` into a datetime
253
+ input and then submitted it as the field's value, suppressing the very
254
+ resolution the declaration asked for (`applyFieldDefaults` only fills fields
255
+ that arrive empty). Those are now left empty for the server.
256
+ - **Callers still win.** `initialData` / `initialValues` outrank a schema
257
+ default — a lookup prefill or a duplicate-record seed is the more specific
258
+ instruction.
259
+
260
+ Only the field-level `defaultValue` is honoured, not a select option's
261
+ `default: true`, even though `@objectstack/spec`'s `SelectOptionSchema` declares
262
+ that key: the insert path resolves `defaultValue` and nothing else, so seeding
263
+ from option-level `default` would preselect values the server would never have
264
+ applied — a UI-only second default contract.
265
+
266
+ - 5bfaabd: `PageComponentSchema.dataSource` now reaches every object-bound block, not just
267
+ `list-view` — and `element:record_picker` stops discarding `view`
268
+ (objectstack#6953).
269
+
270
+ objectstack#5576 wired the spec's per-element data binding
271
+ (`dataSource: { object, view?, filter?, sort?, limit? }`) to `list-view` and left
272
+ the same declaration inert on every other page component. Two gaps remained, and
273
+ both were silent:
274
+
275
+ - **`element:record_picker` read four of the five keys and dropped `view`.** So
276
+ `dataSource: { object: 'account', view: 'hot' }` — the spec's own example —
277
+ built a picker over EVERY account instead of the rows the saved view selects.
278
+ Nothing threw and nothing rendered an error; the option list was simply wider
279
+ than what was authored, which also means a user could select a record the page
280
+ said was out of scope.
281
+ - **`object-grid` / `object-form` / `object-kanban` / `object-calendar` /
282
+ `object-chart` / `object-metric` / `record:related_list` read none of it.**
283
+ Each gates its fetch on its own `objectName`, and nothing mapped
284
+ `dataSource.object` onto it, so a page written the way the spec documents
285
+ rendered an empty grid / a field-less form / a board with no cards / an empty
286
+ month / an empty chart / a static metric number — with no request and no
287
+ diagnostic anywhere. Spec-valid metadata rendering nothing is the
288
+ objectstack#4413 shape.
289
+
290
+ Composition follows objectstack#5576's landed semantics unchanged on every block:
291
+ a named saved view supplies the baseline, a key written on the component itself
292
+ overrides it, an explicit binding key overrides both, `filter` AND-combines
293
+ ("additional filter criteria" — a binding can narrow a view, never widen it), and
294
+ a `view` name that does not resolve renders a configuration error instead of
295
+ degrading to the object's full scope.
296
+
297
+ - `@object-ui/react` — new `useElementDataSourceSchema(schema, mapping, dataSource?)`
298
+ and `ElementDataSourceGate` apply a resolved binding to the schema keys a given
299
+ block reads, plus `ElementDataSourceErrorPanel` / `ElementDataSourceLoadingPanel`
300
+ for the two non-final states. One precedence table for all blocks rather than
301
+ one copy per block — that copy is how "additional filter criteria" would have
302
+ become two dialects.
303
+ - A mapping names **only** keys its block genuinely reads. A composed value
304
+ written onto a key the block ignores would be accepted and dropped, which is
305
+ the defect being removed, one layer deeper — so a kanban's swimlane `columns`
306
+ never receive a view's field list, and a block with no row cap leaves `limit`
307
+ unmapped. The per-block coverage table, including two residual gaps that are
308
+ named rather than papered over, is in `content/docs/guide/data-source.md`.
309
+
310
+ No behaviour changes for a block that carries no `dataSource`: the binding-free
311
+ path returns the schema by reference, so nothing remounts and nothing refetches.
312
+
313
+ - 022002a: `PageComponentSchema.dataSource` now reaches the remaining object-bound public
314
+ blocks: `object-gantt` / `object-timeline` / `object-map` / `object-pivot` /
315
+ `object-master-detail-form` / `embeddable-form` / `record:line_items`
316
+ (objectstack#7121).
317
+
318
+ objectstack#6953 wired the spec's per-element data binding
319
+ (`dataSource: { object, view?, filter?, sort?, limit? }`) to the eight blocks it
320
+ named and left the same declaration inert on these seven. Each gates its fetch on
321
+ its own object key and nothing mapped `dataSource.object` onto it, so a page
322
+ written the way the spec documents rendered an empty gantt / an empty timeline
323
+ rail / a map with no markers / an empty cross-tab / a field-less form — with no
324
+ request and no diagnostic anywhere. Spec-valid metadata rendering nothing is the
325
+ objectstack#4413 shape.
326
+
327
+ Composition follows objectstack#5576's landed semantics unchanged, through the
328
+ shared `ElementDataSourceGate` (no change to it or to the resolution layer): a
329
+ named saved view supplies the baseline, a key written on the component itself
330
+ overrides it, an explicit binding key overrides both, `filter` AND-combines
331
+ ("additional filter criteria" — a binding can narrow a view, never widen it), and
332
+ a `view` name that does not resolve renders a configuration error on every one of
333
+ these blocks instead of degrading to the object's full scope.
334
+
335
+ Each block maps **only** the keys it genuinely reads, which for this batch means
336
+ several keys stay deliberately unmapped rather than being parked somewhere
337
+ plausible:
338
+
339
+ - `object-gantt` and `object-map` take `object` / `filter` / `sort`; neither has a
340
+ row cap or a field-list read site.
341
+ - `object-pivot` takes `object` / `filter`; a cross-tab orders itself by its own
342
+ row/column grouping and cannot be computed over a truncated page.
343
+ - `object-timeline` takes `object` only — its fetch is
344
+ `find(objectName, { options: { $top: 100 } })`, with no filter/sort read site
345
+ at all, so a named view is error-checked and then contributes nothing.
346
+ - `embeddable-form` and `object-master-detail-form` take `object` only (the
347
+ parent object, in the master-detail case); a form that writes one record has no
348
+ collection query for `filter` / `sort` / `limit` to narrow.
349
+ - `record:line_items` takes `object` onto **`childObject`** — the collection it
350
+ actually lists — and nothing else: its query is the parent FK plus a fixed
351
+ `$top: 500`, and its `columns` are editable `GridColumn` objects rather than a
352
+ field-name projection a view could supply.
353
+
354
+ The per-block coverage table, including every residual gap named above, is in
355
+ `content/docs/guide/data-source.md`.
356
+
357
+ No behaviour change for a block that carries no `dataSource`: the binding-free
358
+ path returns the schema by reference, so nothing remounts and nothing refetches.
359
+
360
+ - 6d762da: The five locale keys behind #3546's eight no-fallback `t()` call sites are now defined in all ten packs, so the built-in-view toasts, the activity-timeline source link, the wizard's required-field toast and the Gantt refresh button's accessible name are translated instead of falling back to English — or, on two surfaces, to the key itself (part of #3546).
361
+
362
+ `scripts/check-i18n-call-site-keys.mjs` measured 258 keys that a `t()` call site asks for and no pack defines. These five were the subset with no working inline default: `console.objectView.cannotEditMetaView`, `console.objectView.cannotDeleteMetaView`, `detail.viewSource`, `gantt.toolbar.refresh` and `wizard.missingRequired`. Adding a `defaultValue` is deliberately not the fix — that mechanism is what kept all 258 invisible for months.
363
+
364
+ **Two of the eight sites really did render the raw key**, and both go through a binding with nothing in front of i18next. `ObjectView.tsx` calls `useObjectTranslation()` directly, so five toasts read `console.objectView.cannotEditMetaView` / `cannotDeleteMetaView` on screen; the `|| 'Built-in views cannot be renamed.'` guards next to them were dead on every path, because i18next answers a miss with the key itself and a non-empty string never falls through `||`. Those four unreachable English strings are removed rather than repaired: one key served four call sites (rename / pin / set-as-default / configure), so the pack copy covers any change to a built-in view instead of naming one operation. `RecordActivityTimeline.tsx` fails the same way for a subtler reason — `useDetailTranslation` is `createSafeTranslation(..., 'detail.back')`, and because `detail.back` does resolve, the probe hands back i18next's `t` for every key and bypasses the defaults map wholesale, so `detail.viewSource` reached the user verbatim.
365
+
366
+ **The other two sites were not rendering a raw key**, contrary to the issue's description, and are fixed here as the milder "English in all ten languages" class. `wizard.missingRequired` is its own hook's probe key, so the probe failed and `createSafeTranslation` correctly served its English default. `gantt.toolbar.refresh` goes through `useGanttTranslation`, which deliberately does not use `createSafeTranslation` and falls back per key — so the refresh button's `aria-label` was "Refresh", in English, never the key. Screen-reader users heard an English word rather than an identifier; a `zh` session now hears 刷新.
367
+
368
+ Regression cover is provider-mounted on purpose: with no `I18nProvider` the defaults maps answer every one of these keys and the assertions pass while the console is broken, which is precisely the false-green the issue documents. For the two sites whose English output was already correct, `en` cannot discriminate before from after — the `zh` assertions are the ones that pin the fix.
369
+
370
+ - 11c1e71: Resolve a `select` field declared `multiple: true` to the `field:multiselect` widget, so the object form's visible label actually names the chip picker it renders (objectui#3986).
371
+
372
+ `mapFieldTypeToFormType` keyed the widget id on the field's `type` string alone, so an object-schema `{ type: 'select', multiple: true }` picklist — a spec-legal, entirely ordinary shape — became `field:select`. `SelectField` then delegated to `MultiSelectField` on `config.multiple`, so the component that RENDERED was the chip picker while everything keyed on the widget id still answered for the single-value combobox. Above all the label-association declaration (`ComponentMeta.labelling`, objectui#3961), which the form renderer resolves per widget id: the host emitted `<label for>` at the chip row's wrapper `div`, where a `for` is inert — `HTMLLabelElement.control` returns `null`. Visually the field had a label; in the accessibility tree that label named nothing. Measured on the object-form path, `role=group` + accessible name went from 1 for a `multiselect`-typed field (fixed in objectui#3975) to 0 for this one.
373
+
374
+ Declaring `select` itself `labelling: 'group'` was not available: a single-value select's trigger is a labelable `button[role=combobox]` whose `for` association works, and a bare `select` is a builtin the renderer resolves without consulting the registry at all. The fix is therefore at the producer — the widget id now carries the arity, so one place decides which widget renders and the declaration can no longer be addressed to a widget that is not rendering.
375
+
376
+ - `mapFieldTypeToFormType(fieldType, config?)` takes an optional second argument — the rest of the field definition, of which only `multiple` is read. Existing single-argument calls are unchanged, and so is every type outside the new table: `select` is the only one whose `multiple` form is a different WIDGET. The spec's multi-capable set is larger (select / lookup / file / image, with `radio` on the select branch and `user` storing like `lookup`), but `LookupField`, `FileField` and `ImageField` each render both arities themselves, so their id — and their labelling declaration — is already right for either.
377
+ - The four object-form producers pass the pair: `ObjectForm`, `DrawerForm`, `ModalForm`, and `sectionFields` (Tabbed / Wizard / Split / Drawer / Modal). In `sectionFields` the id is now computed once from the EFFECTIVE pair, after view-level overrides have merged, because `multiple` is itself a spec `FormField` key: a view restating only `multiple: true` over a single-value object field moves the widget too, and `multiple: false` moves it back.
378
+ - `SelectField`'s `multiple` delegation is KEPT, not retired. Measured, it stays reachable from three entrances that never consult the alias map: the inline grid editor (`FieldEditWidget` finds `select` in its own table first), `ActionParamDialog` (`resolveFormWidgetType` returns `select` from `fieldWidgetMap` first), and hand-written SDUI addressing `field:select` by name with `multiple` on its metadata.
379
+
380
+ Read-only rendering of these widgets is untouched (objectui#4005), as is the built-in `Select` branch (objectui#3976).
381
+
382
+ - 523be48: `object-timeline` and `record:line_items` now apply the filter / sort / row cap they are given, so a named `dataSource.view` narrows them instead of contributing nothing
383
+
384
+ These were the two residual gaps in objectstack#7121's per-block coverage table
385
+ (objectstack#7137). Both blocks are object-bound lists, both accepted the spec's
386
+ per-element `dataSource` binding, and neither had a read site for `filter` or
387
+ `sort` anywhere in its fetch:
388
+
389
+ - `object-timeline`'s entire query was
390
+ `find(objectName, { options: { $top: 100 } })`.
391
+ - `record:line_items`' was the parent FK plus a fixed `$top: 500`.
392
+
393
+ So `dataSource: { object, view: 'hot' }` resolved the view — a typo still reported
394
+ a configuration error, it never degraded into an unfiltered query — and then
395
+ dropped everything the view said. The rendered rows could be **wider than the view
396
+ they named**, silently, which is exactly the class of mistake AI-authored metadata
397
+ hides best: the page looks like it works. objectstack#7121 deliberately left the
398
+ keys unmapped and recorded the gap rather than writing composed values onto schema
399
+ keys nobody read; this closes it at the fetch instead.
400
+
401
+ What each block now reads:
402
+
403
+ - **`object-timeline`** — `$filter: schema.filter`,
404
+ `$orderby: convertSortToQueryParams(schema.sort)`, and
405
+ `$top: schema.limit ?? 100`, matching the form `object-gantt` / `object-map` /
406
+ `object-calendar` already use. Its registry mapping gains
407
+ `filter` / `sort` / `limit`; `columns` stays unmapped, because a timeline
408
+ projects the fields its `timeline` config names.
409
+ - **`record:line_items`** — the composed filter is **AND-combined** with the parent
410
+ relationship condition through `mergeFilterNodes`, never substituted for it, the
411
+ same way `record:related_list` composes its own since objectstack#7118: a
412
+ line-items panel is always scoped to the record it sits on, so an _additional_
413
+ criterion can only narrow this parent's children and can never surface another
414
+ parent's rows. `sort` becomes the load order and `limit` the row cap (default
415
+ 500). `columns` stays unmapped — here they are `GridColumn[]` driving an editable
416
+ grid, not a field-name projection, so a view's column list would be the wrong
417
+ _shape_ rather than merely a wider answer.
418
+
419
+ **Behaviour change worth knowing about:** the timeline's default window is now a
420
+ real cap. `{ options: { $top: 100 } }` nested the limit under a key that is not a
421
+ `QueryParams` field and that no adapter in this repo reads (`convertQueryParams`
422
+ maps `params.$top`), so the intended window never reached the wire and a timeline
423
+ over a large object fetched whatever the server chose to return. It is now sent as
424
+ `$top`, and authorable via `limit` or a view's `pagination.pageSize`.
425
+
426
+ `@object-ui/core` gains `convertSortToQueryParams`, the sort→`$orderby` lowering
427
+ the three sibling blocks each inline privately. It is shared rather than copied
428
+ twice more, and is slightly more faithful to the declared contract than those
429
+ copies: a sort entry that omits `order` means ascending instead of being dropped
430
+ (the string spelling `"amount"` already meant ascending in the same copies), and
431
+ nothing orderable yields `undefined` rather than a truthy empty `{}`. Migrating
432
+ the three existing copies onto it is objectstack#7148 and is not done here.
433
+
434
+ - Updated dependencies [794c497]
435
+ - Updated dependencies [993336f]
436
+ - Updated dependencies [f0a625a]
437
+ - Updated dependencies [b5980f4]
438
+ - Updated dependencies [8aad9fd]
439
+ - Updated dependencies [6719877]
440
+ - Updated dependencies [56ff091]
441
+ - Updated dependencies [0186cdc]
442
+ - Updated dependencies [7864f03]
443
+ - Updated dependencies [ea41a59]
444
+ - Updated dependencies [0cbdca8]
445
+ - Updated dependencies [d229dfa]
446
+ - Updated dependencies [ecae400]
447
+ - Updated dependencies [4bc6c23]
448
+ - Updated dependencies [d3e738a]
449
+ - Updated dependencies [c3b01a7]
450
+ - Updated dependencies [f5f8744]
451
+ - Updated dependencies [7ed3360]
452
+ - Updated dependencies [69becd2]
453
+ - Updated dependencies [5e52495]
454
+ - Updated dependencies [0fa5e4d]
455
+ - Updated dependencies [b750823]
456
+ - Updated dependencies [5bfaabd]
457
+ - Updated dependencies [e06810e]
458
+ - Updated dependencies [ab3ad4f]
459
+ - Updated dependencies [65bb513]
460
+ - Updated dependencies [c97a45e]
461
+ - Updated dependencies [b19162d]
462
+ - Updated dependencies [c2fd122]
463
+ - Updated dependencies [1bd6faa]
464
+ - Updated dependencies [ac2139c]
465
+ - Updated dependencies [b14ab3a]
466
+ - Updated dependencies [e24d767]
467
+ - Updated dependencies [8c60819]
468
+ - Updated dependencies [aca561a]
469
+ - Updated dependencies [e64a52e]
470
+ - Updated dependencies [844d17f]
471
+ - Updated dependencies [d8a0be4]
472
+ - Updated dependencies [48132f7]
473
+ - Updated dependencies [4dcd52a]
474
+ - Updated dependencies [42ae5c6]
475
+ - Updated dependencies [0ef9dfd]
476
+ - Updated dependencies [f4b97c8]
477
+ - Updated dependencies [1d723e3]
478
+ - Updated dependencies [0109f54]
479
+ - Updated dependencies [7e5bb5d]
480
+ - Updated dependencies [fbc23e0]
481
+ - Updated dependencies [6d762da]
482
+ - Updated dependencies [e6fdbdc]
483
+ - Updated dependencies [54233b1]
484
+ - Updated dependencies [c2ecbae]
485
+ - Updated dependencies [f9faa7d]
486
+ - Updated dependencies [97b63d7]
487
+ - Updated dependencies [6bb454a]
488
+ - Updated dependencies [11c1e71]
489
+ - Updated dependencies [523be48]
490
+ - Updated dependencies [7e2b7e9]
491
+ - Updated dependencies [33526fd]
492
+ - Updated dependencies [32413ec]
493
+ - Updated dependencies [c1e1e6b]
494
+ - @object-ui/components@17.4.0
495
+ - @object-ui/react@17.4.0
496
+ - @object-ui/core@17.4.0
497
+ - @object-ui/fields@17.4.0
498
+ - @object-ui/i18n@17.4.0
499
+ - @object-ui/types@17.4.0
500
+ - @object-ui/permissions@17.4.0
501
+
3
502
  ## 17.3.0
4
503
 
5
504
  ### Minor Changes
@@ -1268,6 +1767,7 @@ undefined` — swallowing the very errors the command exists to print. Now reads
1268
1767
  objects+fields were editable in Studio; this reworks both surfaces.
1269
1768
 
1270
1769
  **Setup (assign + read-only):**
1770
+
1271
1771
  - The six facets (`object_permissions`, `field_permissions`, `system_permissions`,
1272
1772
  `row_level_security`, `tab_permissions`, `admin_scope`) now render read-only on
1273
1773
  the `sys_permission_set` record page as a compact summary (counts, or capability
@@ -1282,6 +1782,7 @@ undefined` — swallowing the very errors the command exists to print. Now reads
1282
1782
 
1283
1783
  **Studio (design every facet):** the permission matrix editor gains structured
1284
1784
  editors for the facets that were JSON-only —
1785
+
1285
1786
  - **System Capabilities**: a multi-select over the live `sys_capability` registry
1286
1787
  (scope-grouped, labelled chips).
1287
1788
  - **Row-Level Security**: per-policy rows (object · operation · enabled) with CEL
@@ -2459,6 +2960,7 @@ parentData, details)` — emits a parent `update` op (index 0) then diffs each
2459
2960
  ### Patch Changes
2460
2961
 
2461
2962
  - 89ae109: Fix click navigation and required-FK form rendering
2963
+
2462
2964
  - **plugin-grid**: ObjectGrid's `getSelectFields()` now always includes `id` in
2463
2965
  the SELECT projection. Previously, when a view configured `columns` without
2464
2966
  `id`, the SQL driver stripped it from results, and row-click handlers silently
@@ -2521,6 +3023,7 @@ parentData, details)` — emits a parent `update` op (index 0) then diffs each
2521
3023
  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.
2522
3024
 
2523
3025
  Specifically fixes:
3026
+
2524
3027
  - `@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.
2525
3028
  - `@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.
2526
3029
  - All plugin packages now use a unified `external: (id) => !/^[./]/.test(id) && !id.startsWith(__dirname)` rule, ensuring future additions of CJS deps are automatically externalized.
@@ -2598,11 +3101,13 @@ parentData, details)` — emits a parent `update` op (index 0) then diffs each
2598
3101
  `FormSchema.mobileStickyActions` (new) is the lower-level escape hatch — applied automatically when `mobile.stickyActions` is set on `ObjectFormSchema`.
2599
3102
 
2600
3103
  **`@object-ui/plugin-form`** — `ObjectForm` now:
3104
+
2601
3105
  - propagates `mobile.fullscreenLongText` to every textarea/markdown/html field as `mobile_fullscreen: true`,
2602
3106
  - sets `mobileStickyActions` on the inner form schema and adds `pb-20` padding so content isn't covered by the fixed bar,
2603
3107
  - 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.
2604
3108
 
2605
3109
  **`@object-ui/components`** — the registered `form` renderer adds:
3110
+
2606
3111
  - a `mobileStickyActions` opt-in that turns the action row into a `position: sticky; bottom: 0` bar on small viewports, and
2607
3112
  - 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.
2608
3113
 
package/README.md CHANGED
@@ -83,6 +83,71 @@ interface FormField {
83
83
  }
84
84
  ```
85
85
 
86
+ ### What a create form opens with
87
+
88
+ A create form has no persisted record, so its opening values come from the
89
+ object schema's declared field `defaultValue`s. Every object-form container
90
+ (`ObjectForm`, `ModalForm`, `DrawerForm`, `TabbedForm`, `SplitForm`,
91
+ `WizardForm`) resolves them the same way, through `schemaDefaults`:
92
+
93
+ | Declared | Create form opens with | Why |
94
+ |---|---|---|
95
+ | `defaultValue: 'draft'` (any static literal) | `draft`, preselected and submittable | the value is known; making the user pick it is busywork, and on a status-like field every wrong option is one click away |
96
+ | `defaultValue: 'NOW()'` / `'current_user'` (a runtime token) | empty | the token is an *instruction*, not a value. The server resolves it at insert — but only for fields that arrive empty, so seeding the literal text would suppress it |
97
+ | `defaultValue: cel\`today()\`` (an Expression envelope) | empty | same reason: the server evaluates it per insert |
98
+ | an option's `default: true` | empty | see below |
99
+ | nothing | empty | no default is invented |
100
+
101
+ `initialData` / `initialValues` passed by the caller always outrank a schema
102
+ default — a lookup prefill or a "duplicate this record" seed is the more
103
+ specific instruction.
104
+
105
+ Only the **field-level** `defaultValue` is read, never a select option's
106
+ `default: true`, even though `@objectstack/spec`'s `SelectOptionSchema` declares
107
+ that key. The server's insert path resolves `defaultValue` and nothing else, so
108
+ a form that also seeded from option-level `default` would preselect values the
109
+ server would never have applied on its own — a renderer-side second default
110
+ contract (AGENTS.md #0.1). If option-level `default` is meant to mean "the
111
+ initial value", that belongs at the producer.
112
+
113
+ **Edit forms are never seeded.** An edit form shows the row as the server holds
114
+ it; folding a default in over a column the record leaves unset would arm a
115
+ silent write of a value the user never chose, on the next save of any other
116
+ field.
117
+
118
+ #### `required` + a runtime default
119
+
120
+ A field may declare both, and it is coherent authoring — storage-level required,
121
+ with the value guaranteed by the producer:
122
+
123
+ ```ts
124
+ remind_at: Field.datetime({ required: true, defaultValue: 'NOW()' }),
125
+ ```
126
+
127
+ But the control opens empty (see the table), so enforcing `required` on it
128
+ refused the submit with nothing sensible for the user to type. In **create**
129
+ mode a runtime `defaultValue` therefore suppresses the client-side `required`
130
+ rule, and the field is **omitted from the payload** — omitted, not sent empty,
131
+ because `applyFieldDefaults` resolves the declaration only for a field that
132
+ arrives absent or null, and a blank string is neither.
133
+
134
+ | Mode | Declared | Left empty | Effect |
135
+ |---|---|---|---|
136
+ | create | `required` + a runtime default | yes | submits; the key is absent, and the server resolves it |
137
+ | create | `required` + a runtime default | no (user typed) | submits the typed value — it outranks the default |
138
+ | create | `required` + a static literal | user cleared the seeded control | refused: they removed a value that was really there |
139
+ | edit | `required`, anything | user blanked it | refused: the token was resolved at insert, so this is a real removal |
140
+
141
+ The required marker and `aria-required` go with the rule in the create case,
142
+ since one boolean drives all three — in that mode the user genuinely is not
143
+ required to provide the value. Showing what the server *will* supply, as a
144
+ non-authoritative preview, is a separate follow-up.
145
+
146
+ Both halves read one predicate (`isRuntimeDefault` in `schemaDefaults`), which
147
+ is what keeps a form from seeding a field it also refuses to submit. Not
148
+ extended to `requiredWhen`, the conditional-required CEL rule, which the form
149
+ renderer resolves against the live record.
150
+
86
151
  ### Column width of a sectioned form
87
152
 
88
153
  A sectioned form renders as ONE grid, and two keys decide its shape:
@@ -357,17 +422,6 @@ The plugin includes these field components:
357
422
  - Date picker
358
423
  - File upload
359
424
 
360
- <!-- release-metadata:v3.3.0 -->
361
-
362
- ## Compatibility
363
-
364
- - **React:** 18.x or 19.x
365
- - **Node.js:** ≥ 18
366
- - **TypeScript:** ≥ 5.0 (strict mode)
367
- - **`@objectstack/spec`:** ^3.3.0
368
- - **`@objectstack/client`:** ^3.3.0
369
- - **Tailwind CSS:** ≥ 3.4 (for packages with UI)
370
-
371
425
  ## Links
372
426
 
373
427
  - 📚 [Documentation](https://www.objectui.org/docs/plugins/plugin-form)