@object-ui/plugin-form 16.0.0 → 17.1.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +712 -1
  2. package/README.md +106 -0
  3. package/dist/DrawerForm.d.ts +1 -0
  4. package/dist/DrawerForm.d.ts.map +1 -0
  5. package/dist/EmbeddableForm.d.ts +1 -0
  6. package/dist/EmbeddableForm.d.ts.map +1 -0
  7. package/dist/FormAnalytics.d.ts +1 -0
  8. package/dist/FormAnalytics.d.ts.map +1 -0
  9. package/dist/FormSection.d.ts +1 -0
  10. package/dist/FormSection.d.ts.map +1 -0
  11. package/dist/LineItemsPanel.d.ts +1 -0
  12. package/dist/LineItemsPanel.d.ts.map +1 -0
  13. package/dist/MasterDetailForm.d.ts +9 -1
  14. package/dist/MasterDetailForm.d.ts.map +1 -0
  15. package/dist/ModalForm.d.ts +7 -2
  16. package/dist/ModalForm.d.ts.map +1 -0
  17. package/dist/ObjectForm.d.ts +1 -0
  18. package/dist/ObjectForm.d.ts.map +1 -0
  19. package/dist/SplitForm.d.ts +22 -2
  20. package/dist/SplitForm.d.ts.map +1 -0
  21. package/dist/TabbedForm.d.ts +13 -1
  22. package/dist/TabbedForm.d.ts.map +1 -0
  23. package/dist/WizardForm.d.ts +14 -1
  24. package/dist/WizardForm.d.ts.map +1 -0
  25. package/dist/autoLayout.d.ts +1 -0
  26. package/dist/autoLayout.d.ts.map +1 -0
  27. package/dist/deriveMasterDetail.d.ts +1 -0
  28. package/dist/deriveMasterDetail.d.ts.map +1 -0
  29. package/dist/fieldGroups.d.ts +1 -0
  30. package/dist/fieldGroups.d.ts.map +1 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +1655 -1457
  34. package/dist/index.umd.cjs +1 -1
  35. package/dist/masterDetailTx.d.ts +9 -37
  36. package/dist/masterDetailTx.d.ts.map +1 -0
  37. package/dist/occSave.d.ts +78 -0
  38. package/dist/occSave.d.ts.map +1 -0
  39. package/dist/sanitize.d.ts +1 -0
  40. package/dist/sanitize.d.ts.map +1 -0
  41. package/dist/sectionFields.d.ts +10 -2
  42. package/dist/sectionFields.d.ts.map +1 -0
  43. package/dist/successBehavior.d.ts +1 -0
  44. package/dist/successBehavior.d.ts.map +1 -0
  45. package/package.json +14 -13
package/CHANGELOG.md CHANGED
@@ -1,5 +1,716 @@
1
1
  # @object-ui/plugin-form
2
2
 
3
+ ## 17.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 38ca8be: refactor(fields): `requiredWhen` is the only required-predicate slot — drop the retired `conditionalRequired` alias
8
+
9
+ `@objectstack/spec` 17 (objectstack#3855) **retired** `Field.conditionalRequired`,
10
+ the long-deprecated alias of `requiredWhen`. ObjectUI carried a back-compat read
11
+ for it in seven places; all of them are removed.
12
+
13
+ The removal is safe because the spec did not merely _stop emitting_ the key — it
14
+ made authoring it **fail loudly**. `retiredKey()` declares the key as
15
+ `z.never()`, so:
16
+
17
+ - `z.input` types it as `never` — writing it is a `tsc` error at the authoring site;
18
+ - the parse **rejects** it (verified against `17.0.0-rc.0`), at both `FieldSchema`
19
+ and `ObjectSchema`, with the prescription as the message:
20
+
21
+ > `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use
22
+ > `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged.
23
+ > Run `os migrate meta --from 16` to rewrite it automatically.
24
+
25
+ So spec-parsed metadata cannot carry the key — an object declaring it fails to
26
+ load rather than loading with the rule silently dropped. Keeping a renderer-side
27
+ `requiredWhen ?? conditionalRequired` would have re-created exactly the second
28
+ de-facto contract the tombstone exists to prevent: the key would have kept
29
+ working in the UI while being rejected everywhere else, hiding the producer's bug
30
+ (AGENTS.md #0.1). "Backend-agnostic" (#1) does not argue for keeping it either —
31
+ `conditionalRequired` is an ObjectStack-spec-ism, so the only producers that ever
32
+ emit it are ObjectStack producers on ≤16, and the spec ships them a converter.
33
+
34
+ Removed from:
35
+
36
+ | package | site |
37
+ | :----------------------- | :-------------------------------------------------------------------------------------------------------- |
38
+ | `@object-ui/types` | the `conditionalRequired?:` member on `FormField` |
39
+ | `@object-ui/core` | the `??` fallback + rules-param member in `resolveFieldRuleState` |
40
+ | `@object-ui/components` | three pass-throughs in the form renderer |
41
+ | `@object-ui/plugin-form` | `ObjectForm`, `ModalForm`, `sectionFields`, `deriveMasterDetail` (×2) |
42
+ | `@object-ui/app-shell` | the field inspector's legacy read/auto-migrate, and the key's entry in `clientValidation`'s CEL lint list |
43
+
44
+ **Studio authors lose nothing.** The object designer's draft validation parses
45
+ against the spec's own `ObjectSchema`, so a draft carrying the key now surfaces
46
+ the tombstone's rename prescription under the same `fields.<name>.conditionalRequired`
47
+ path the CEL lint used to report — a better message than the inspector's silent
48
+ auto-migration, and one the server agrees with. That behavior is pinned by a test.
49
+
50
+ **Migrating:** rename the key to `requiredWhen` (the CEL value is unchanged), or
51
+ run `os migrate meta --from 16`.
52
+
53
+ - 03bd53b: feat(form): `SplitForm` honours the spec's new `FormSection.pane`
54
+
55
+ A split form's panel assignment was a hardcoded positional rule — first section
56
+ left, everything else right. The rule was invisible in the metadata, so
57
+ reordering sections silently moved them across the divider, and an author could
58
+ not place two sections in the left pane at all.
59
+
60
+ Sections now declare their panel: `pane: 'primary' | 'secondary'`
61
+ (@objectstack/spec `FormSection.pane`, objectstack#4160). Placement follows the
62
+ key, not the array position — reordering paned sections never changes the
63
+ layout. Omitted keys keep the exact legacy rule (first section `primary`, rest
64
+ `secondary`), so existing metadata renders unchanged.
65
+
66
+ `ObjectForm`'s split dispatch copies the key through its per-key section mapping
67
+ (the path that once silently dropped `visibleOn`), and `ObjectFormSection`
68
+ declares it. The spec side rejects `pane` on non-split form types at parse, so
69
+ the key can never be an accepted-but-ignored no-op.
70
+
71
+ ### Patch Changes
72
+
73
+ - 7639a61: fix(form): the spec↔runtime form-field chokepoint stops dropping spec 17 vocabulary, and the validator stops contradicting the renderer — #3090
74
+
75
+ `normalizeSectionField` — the one translation point between the spec's authored
76
+ form-field shape (`field` = object-field reference) and the runtime shape
77
+ (`name` = data path) — silently dropped four spec keys, worst of all the
78
+ ADR-0089 **canonical** `visibleWhen` spelling while the deprecated `visibleOn`
79
+ worked. Now:
80
+
81
+ - view-level `visibleWhen` routes into the view-level slot (`visibleOn`) so it
82
+ ANDs with the object-level rule instead of clobbering it, and the wizard's
83
+ final-submit gate folds the same slot into its verdict (before, a required
84
+ field the view itself hides could block submission from off-screen);
85
+ - `dependsOn`, `keyField`, and `disclosure` carry through;
86
+ - a behavioral parity gate walks the spec `FormFieldSchema` key set — a key the
87
+ spec adds fails as unmapped, a key it retires fails as stale.
88
+
89
+ `SelectOptionSchema` is now derived from `@objectstack/spec/data` by reference
90
+ (it used to strip `color` — which `@object-ui/fields` renders — plus `default`
91
+ and the per-option `visibleWhen` gate), with pinned divergences (`value`
92
+ widened for UI forms, `visibleWhen` on the #2212 wire contract) and documented
93
+ UI-only extensions (`disabled`, `icon`). `SelectOption` (TS) gains `color` and
94
+ `default`.
95
+
96
+ `FormFieldSchema` (the runtime vocabulary `objectui validate` enforces) now
97
+ covers every key the `FormField` interface declares — `widget`, `dependsOn`,
98
+ `hidden`, `readonly`, `visibleOn`/`visibleWhen`/`readonlyWhen`/`requiredWhen`,
99
+ `span` — and `type` is optional, matching the interface. A typo'd predicate now
100
+ fails loudly instead of being stripped; spec-shape fields (`{ field: … }`) are
101
+ still rejected, pinning the two-layer boundary.
102
+
103
+ - 94e63ef: fix(form): the runtime `field` metadata slot is declared instead of smuggled, and importing the spec's FormField is a lint error — #3090
104
+
105
+ `FormField.field` — the slot where object-bound form paths stash the resolved
106
+ field-metadata **object** for widgets — rode through the index signature,
107
+ undeclared, readable only via `as any`. Same key, different layer: in the spec
108
+ form-view vocabulary `field` is a _string_ (the referenced object-field name),
109
+ and the undeclared slot kept that pun latent. The slot is now declared
110
+ (`field?: Record<string, any>`) with the invariant in its JSDoc: on a runtime
111
+ FormField it is never a string — the authored string form ends at the
112
+ `normalizeSectionField` chokepoint, and a tripwire test pins that across all
113
+ three input shapes. Assigning a string is now a compile error; the `as any`
114
+ casts at the read sites are gone.
115
+
116
+ A `no-restricted-imports` tripwire bans importing `FormField`/
117
+ `FormFieldSchema` from `@objectstack/spec/ui` inside this repo: the spec's
118
+ FormField TYPE erases to `any` in its dist (objectstack#4171), so the
119
+ misimport silently deletes type safety — tsc says nothing. The lint message
120
+ names the two layers and the correct import. The drift-guard parity test is
121
+ the one legitimate importer, exempted inline with its reason.
122
+
123
+ Ledger: `FormField` and `FormFieldSchema` move from untriaged DEBT to ALLOW
124
+ with the two-layer rationale written down (122 → 120).
125
+
126
+ - aeb0bd2: fix(form): a tabbed/split form honours the form view's own `columns`
127
+
128
+ `FormView.columns` is a spec key, but only `ObjectForm`'s simple path and
129
+ `ModalForm` read it. `TabbedForm` and `SplitForm` derived the grid width from the
130
+ per-section `columns` alone, so a view declaring `columns: 3` rendered 3 columns
131
+ in a modal and **single-column** as a tab or split — the same metadata laying out
132
+ differently depending on which host picked it up.
133
+
134
+ Both now resolve the grid the way the other hosts already did:
135
+
136
+ explicit form `columns` ?? widest section's `columns` ?? 1
137
+
138
+ The two keys answer different questions and the precedence reflects that: the
139
+ view's `columns` is how wide the grid is, a section's `columns` is how densely
140
+ that section fills it (via per-field `colSpan`). `columns` is declared on
141
+ `TabbedFormSchema` / `SplitFormSchema` accordingly — `ObjectForm` already spread
142
+ it through, it was simply being dropped on arrival.
143
+
144
+ - c735bf7: fix(form): a spec-vocabulary field no longer crashes the standalone form, and every surface now says which vocabulary you meant — #3090
145
+
146
+ Writing the regression test against the unfixed renderer proved the failure
147
+ was worse than the assumed silent drop: a `{ field: 'x' }` entry (spec
148
+ form-VIEW vocabulary) slipped past the `f?.name` guards into a
149
+ react-hook-form Controller with `name === undefined` and crashed the whole
150
+ standalone form on `name.split('.')`, with nothing naming the culprit entry.
151
+ The renderer now partitions such entries out — the rest of the form renders —
152
+ and surfaces them with an inline alert plus a console.error whose text is the
153
+ fix instruction (rename to `name`, or use an object-bound form whose sections
154
+ accept the spec shape).
155
+
156
+ `objectui validate` grows the same boundary awareness: on failure, a
157
+ `{ field: … }` entry in a standalone form gets a "likely cause" hint naming
158
+ the real fix instead of the bare `invalid_union` — the previous message read
159
+ as "bolt a `name` on", which converts spec metadata wrongly. On success,
160
+ mixed-vocabulary entries (`name` + string `field`) get a warning: they
161
+ validate, but the spec key is dead weight the renderer ignores.
162
+
163
+ `normalizeSectionField` warns (once per site) when an authored section field
164
+ mixes both identity keys — the spec branch derives the runtime name from
165
+ `field`, so an authored `name` was silently overwritten.
166
+
167
+ - e339d60: fix(plugin-form): swapping `recordId` no longer leaves the previous record on screen
168
+
169
+ `loading` in `ModalForm` / `DrawerForm` / `TabbedForm` / `SplitForm` was only ever
170
+ set `true` once, by `useState(true)`, and thereafter only ever set `false`. A
171
+ `recordId` change therefore re-entered the fetch effect **without** going back
172
+ through the loading branch: the form stayed mounted showing — and accepting edits
173
+ to — record A's values, with nothing indicating a different record had been asked
174
+ for, until B's response landed and replaced them in place. Anything typed in that
175
+ window read as A's on screen and would have been submitted against B.
176
+
177
+ The same effect had no staleness guard either, so two overlapping reads landed in
178
+ **completion** order rather than request order: ask for B then C, and a slow B
179
+ arriving last left the form showing B while the caller had asked for C.
180
+
181
+ Both are the same defect from the user's side — the form displays a record nobody
182
+ asked for — so both are fixed:
183
+
184
+ - a change of record re-enters the loading state before the read, so the previous
185
+ record is off screen while the next one is in flight. Gated on the record
186
+ actually changing: the effect also re-runs on `initialData`/`initialValues`
187
+ identity churn (callers rebuild those objects every render), and flashing the
188
+ loading state for that would thrash;
189
+ - the effect's cleanup marks its read stale, so a response that is no longer the
190
+ one being awaited is dropped instead of overwriting a newer record.
191
+
192
+ `ObjectForm` already re-entered loading before its fetch, which is why this only
193
+ ever reproduced on the four sectioned variants.
194
+
195
+ **Also fixed, a consequence of the above:** hiding the form unmounts the inner
196
+ renderer, and that renderer is the only thing that reports dirtiness via
197
+ `onDirtyChange` — it gets no chance to report `false` on the way out. Without
198
+ clearing the flag, the overlay's unsaved-input guards would stay armed for input
199
+ belonging to a record no longer on screen: a plain refresh would prompt, and
200
+ closing would offer to discard nothing.
201
+
202
+ - aa35561: fix(form): a split create/edit form no longer loses the panel you are not submitting from (#2153)
203
+
204
+ `SplitForm` rendered one `SchemaRenderer` — one react-hook-form instance and one
205
+ `<form>` element — **per section**, and its two groups of sections live in
206
+ separate resizable panels. So each panel owned isolated form state: submitting
207
+ from one panel's action bar sent only that section's fields and silently dropped
208
+ everything the user had typed on the other side of the divider. Filling both
209
+ panels and clicking Create persisted `{ subject }` alone.
210
+
211
+ The same isolation killed cross-panel field rules: a `visibleWhen` in the right
212
+ panel referencing a left-panel field never saw that field in its record, so the
213
+ predicate faulted and failed **open** — the field the author meant to hide was
214
+ always shown.
215
+
216
+ Both panels are now ONE form. The panel group became a layout the form renderer
217
+ owns, via a new `FormSchema.fieldPanes` (+ `fieldPanesOrientation`,
218
+ `fieldPanesResizable`) that mirrors `fieldTabs` (#2959): the `<form>` wraps the
219
+ whole `ResizablePanelGroup` and each pane holds only fields, which is what lets a
220
+ single react-hook-form instance span the divider. Sections inside a pane render
221
+ behind the inline `section-divider` header, each at its own declared column
222
+ density within the form's shared grid.
223
+
224
+ One more fix falls out of moving the panels into the renderer: `splitResizable:
225
+ false` now actually pins the divider. It previously only hid the grip — the
226
+ separator stayed draggable, because nothing passed the panel library's
227
+ `disabled`.
228
+
229
+ Each pane is its own `@container`, so a multi-column section collapses to fewer
230
+ columns as its panel is dragged narrower instead of overflowing.
231
+
232
+ - 3c1f321: fix(form): a tabbed/sectioned create-edit form no longer loses the tabs you are not looking at (#2959, #2153)
233
+
234
+ The explicit-`sections` path rendered one `SchemaRenderer` — one react-hook-form
235
+ instance and one `<form>` element — **per section**, all sharing the same
236
+ `formId`. Two failures compounded:
237
+
238
+ 1. the footer submit button (`form={formId}`) can only be associated with the
239
+ **first** of those forms, so section 2+ never reached the payload; and
240
+ 2. in the `tabbed` variant Radix unmounted the inactive panel, destroying that
241
+ tab's form state outright.
242
+
243
+ Reported flow (HotCRM, 3 tabs, required `description` on tab 3): fill tab 1 →
244
+ submit → server 400 `description is required` → switch to tab 3, fill it →
245
+ submit → the server now reports `subject; description; status; priority` **all**
246
+ missing, because the second submit's body had lost every earlier value.
247
+
248
+ `ModalForm` (stacked and `contentLayout: 'tabbed'`) and `TabbedForm` now render
249
+ ONE form for all sections, matching `ObjectForm` / `DrawerForm`. Stacked sections
250
+ use the existing inline `section-divider` header (which now also renders the
251
+ section's `description`); tabbed sections go through a new
252
+ `FormSchema.fieldTabs` (+ `defaultFieldTab`, `fieldTabsPosition`) that the form
253
+ renderer distributes into **force-mounted** Radix panels — CSS-hidden rather
254
+ than unmounted, since react-hook-form skips validation for unmounted fields,
255
+ which is how a required field on a tab nobody opened used to sail past the
256
+ client and come back as a server 400.
257
+
258
+ Validation feedback now points at the tab: a rejected field activates its tab and
259
+ every tab holding one is marked on its trigger, for client-side rules and server
260
+ `fields[]` rejections alike.
261
+
262
+ - c0d0bc8: fix(form): a wizard with `allowSkip` no longer submits past the required fields you skipped
263
+
264
+ `allowSkip` let the user jump to any step from the indicator, and
265
+ `handleStepClick` did so without validating anything on the way. Since a wizard
266
+ mounts ONE step at a time and react-hook-form only validates the fields currently
267
+ **mounted**, a required field on a step nobody opened was never registered, never
268
+ validated, and simply absent from the payload.
269
+
270
+ Measured against the unfixed component — 3 steps, required `owner` on step 2,
271
+ `allowSkip: true`, click step 3's indicator, fill it, hit Create:
272
+
273
+ createCalls: 1
274
+ payload: { subject: 'S1', notes: 'S3' } // `owner` missing entirely
275
+ UI mentions "required": false // nothing said so
276
+
277
+ So an invalid create went out and the client said nothing about why — #2959's
278
+ validation half, wearing a wizard's clothes.
279
+
280
+ The final submit now checks the WHOLE declared field set, and when something is
281
+ outstanding it returns the user to the first step that has one, marks that step's
282
+ indicator (`data-error="true"`, destructive circle + icon), names the fields in a
283
+ toast, and sends nothing. Conditional rules are honoured: the check runs on the
284
+ canonical `resolveFieldRuleState`, the same engine the form renderer and the
285
+ server's rule-validator use, so a field hidden by `visibleWhen` or not yet
286
+ required by `requiredWhen` is not demanded. The sequential path is unaffected —
287
+ a forward jump is refused without `allowSkip`, so Next already validated each step.
288
+
289
+ Also in `WizardForm`:
290
+
291
+ - `FormView.columns` is now honoured (spec key, previously dropped): the grid
292
+ width is the view's `columns`, else the step's own. Unlike the tabbed/split
293
+ hosts there is no widest-section fallback — wizard steps never share a viewport,
294
+ so each keeps its authored width.
295
+ - the root gained `@container`. The step grid is sized with container queries, and
296
+ without a container ancestor every `@md:`/`@2xl:` variant was inert — a step
297
+ declaring 2 columns rendered single-column. Found by running it in a browser;
298
+ the class was present all along, which is why asserting the class alone had
299
+ missed it.
300
+
301
+ - Updated dependencies [62311b6]
302
+ - Updated dependencies [fc0272a]
303
+ - Updated dependencies [9e7349e]
304
+ - Updated dependencies [8864971]
305
+ - Updated dependencies [1cf0de7]
306
+ - Updated dependencies [752e18f]
307
+ - Updated dependencies [c785740]
308
+ - Updated dependencies [b41f401]
309
+ - Updated dependencies [19e9fa0]
310
+ - Updated dependencies [d61efd1]
311
+ - Updated dependencies [95b7214]
312
+ - Updated dependencies [7d9734d]
313
+ - Updated dependencies [6ae818e]
314
+ - Updated dependencies [9eb932b]
315
+ - Updated dependencies [746dd00]
316
+ - Updated dependencies [aebfa4f]
317
+ - Updated dependencies [38ca8be]
318
+ - Updated dependencies [3cb9646]
319
+ - Updated dependencies [68ef584]
320
+ - Updated dependencies [4952edf]
321
+ - Updated dependencies [7f0252e]
322
+ - Updated dependencies [c4d7b20]
323
+ - Updated dependencies [c769d3d]
324
+ - Updated dependencies [7639a61]
325
+ - Updated dependencies [94e63ef]
326
+ - Updated dependencies [c735bf7]
327
+ - Updated dependencies [02aef0c]
328
+ - Updated dependencies [6f29aa5]
329
+ - Updated dependencies [d21794c]
330
+ - Updated dependencies [c4db402]
331
+ - Updated dependencies [5319bf1]
332
+ - Updated dependencies [49e5671]
333
+ - Updated dependencies [2307b52]
334
+ - Updated dependencies [9a04d25]
335
+ - Updated dependencies [b5b97e2]
336
+ - Updated dependencies [f59f2c1]
337
+ - Updated dependencies [07de839]
338
+ - Updated dependencies [2a40b5e]
339
+ - Updated dependencies [df613fa]
340
+ - Updated dependencies [4874117]
341
+ - Updated dependencies [ad0183a]
342
+ - Updated dependencies [ce08d55]
343
+ - Updated dependencies [eb4b740]
344
+ - Updated dependencies [aecc934]
345
+ - Updated dependencies [5b084eb]
346
+ - Updated dependencies [aa1240a]
347
+ - Updated dependencies [2374a49]
348
+ - Updated dependencies [390c071]
349
+ - Updated dependencies [d10f526]
350
+ - Updated dependencies [2d5d594]
351
+ - Updated dependencies [ea7f477]
352
+ - Updated dependencies [379728f]
353
+ - Updated dependencies [7f23cd0]
354
+ - Updated dependencies [0ded602]
355
+ - Updated dependencies [24e0e0a]
356
+ - Updated dependencies [f8a95e5]
357
+ - Updated dependencies [3a6cf24]
358
+ - Updated dependencies [aa35561]
359
+ - Updated dependencies [03bd53b]
360
+ - Updated dependencies [3c1f321]
361
+ - Updated dependencies [a045a32]
362
+ - Updated dependencies [912496d]
363
+ - Updated dependencies [80edbd4]
364
+ - Updated dependencies [9867281]
365
+ - @object-ui/core@17.1.0
366
+ - @object-ui/components@17.1.0
367
+ - @object-ui/react@17.1.0
368
+ - @object-ui/types@17.1.0
369
+ - @object-ui/i18n@17.1.0
370
+ - @object-ui/permissions@17.1.0
371
+ - @object-ui/fields@17.1.0
372
+
373
+ ## 17.0.0
374
+
375
+ ### Minor Changes
376
+
377
+ - f9bbddb: feat: gate detail/form edit & delete on the server's effective operation set (#3546)
378
+
379
+ PR-4 (#3391) wired the **list/toolbar** surface (ObjectView Import, ListView /
380
+ ObjectGrid Export) to the server-resolved effective API operation set
381
+ (`/me/permissions` `apiOperations`, intersected via
382
+ `resolveCrudAffordances(obj, effectiveApiOperations?)`). The **detail / form**
383
+ surfaces still gated edit/delete on the bucket + `userActions` alone. This
384
+ extends the same intersection to them, so the record page and its forms never
385
+ offer an operation the server would 405.
386
+
387
+ - **core** `isObjectInlineEditable(obj, effectiveApiOperations?)` gains the same
388
+ optional second argument as `resolveCrudAffordances` — inline-edit is now
389
+ additionally ANDed with the server allowing `update`.
390
+ - **app-shell** `RecordDetailView` threads the object's effective operations into
391
+ the synthesized Edit/Delete header actions and the record-body inline-edit
392
+ gate (`canEdit`); `RelatedRecordActionsBridge` intersects each **child**
393
+ object's Create/Edit/Delete handlers with that child's own effective set.
394
+ - **plugin-detail** `record:details` ANDs its inline-edit affordance with the
395
+ object's effective `update`.
396
+ - **plugin-form** `ObjectForm`'s blanket managed-object field lock also engages
397
+ when the server denies `update` (edit mode) / `create` (create mode).
398
+
399
+ Backward-compatible: a missing effective set (unrestricted object, older
400
+ backend, or no `PermissionProvider`) leaves the resolved affordance untouched —
401
+ the bucket/`userActions` decision wins, exactly as today. Layers on top of the
402
+ existing per-object `check('edit')` / `check('delete')` permission gates
403
+ (intersection, never union).
404
+
405
+ ### Patch Changes
406
+
407
+ - 6dee2cb: feat(form): consume spec-aligned FormView buttons/defaults in ObjectForm
408
+
409
+ The authored `@objectstack/spec` FormViewSchema carries structured
410
+ `buttons.{submit,cancel,reset}.{show,label}` and `defaults`, but the form
411
+ renderer only read the flat renderer-invented `showSubmit`/`submitText`/
412
+ `showCancel`/`cancelText`/`showReset`/`initialValues`. That left the two spec
413
+ keys parsed-but-inert (ADR-0078) and stuck at `experimental` in the spec
414
+ liveness ledger.
415
+
416
+ `ObjectForm` now folds the structured shape down onto those flat props inside
417
+ its existing normalization pass, so every entry path (ObjectView
418
+ drawer/modal/page, RecordFormPage) honors it. An explicitly-set flat key still
419
+ wins, so metadata authored against the deprecated flat keys is unchanged.
420
+ `ObjectView` and `RecordFormPage` forward `buttons`/`defaults` from the spec
421
+ form view. `ObjectFormSchema` gains the optional `buttons`/`defaults` fields.
422
+
423
+ Refs objectstack-ai/objectstack#1894, objectstack-ai/objectstack#2998.
424
+
425
+ - 2f947e4: fix(page,field): consume the spec's `type`/`label`/`maxLength` keys (framework#1878 §3 naming-drift recheck)
426
+
427
+ Three forward-drifts where objectui read a different key than the spec
428
+ declares, so authoring the documented key silently no-oped:
429
+
430
+ - **page `type` → `pageType`** (app-shell + components): `PageSchema` declares
431
+ the page KIND as `type`, but `PageRenderer` reads `schema.pageType` and fell
432
+ back to `'record'` — and nothing mapped between them. Every non-record page
433
+ (`home`/`app`/`list`/`utility`) rendered with the record max-width, a wrong
434
+ `data-page-type` attribute, and a suppressed header. `PageView` now passes
435
+ `pageType` alongside the SchemaNode discriminator `type`.
436
+ - **page `label` → `title`** (components): `PageSchema.label` is required but the
437
+ region renderer read only `title`. Now dual-reads `title ?? label`, mirroring
438
+ the fallback `DashboardRenderer` already uses. Coupled with the above — the
439
+ header is gated on `pageType !== 'record'`, so both were needed for a title to
440
+ appear.
441
+ - **field `maxLength`/`minLength`** (plugin-form + fields): validation already
442
+ dual-read these, but `ObjectForm`'s HTML-attribute pass and `TextAreaField`
443
+ read `max_length` only, so a spec-authored `maxLength` gave no browser cap and
444
+ no character counter. Both now dual-read, matching `buildValidationRules`.
445
+
446
+ Verified in the browser against the showcase: `capability_map` (`type: 'home'`)
447
+ now renders `data-page-type="home"`, the `home` max-width and its page title;
448
+ record pages are unchanged.
449
+
450
+ - 662bdf9: fix(fls): wire the real per-caller FLS channel into import targets and grid
451
+ columns; remove the never-populated `field.permissions` shape (objectstack#3661)
452
+
453
+ The `permissions?: { read?, write?, edit? }` key on `@object-ui/types` field
454
+ definitions (Phase 3.2.6) was declared-but-never-enforced: no producer in the
455
+ stack ever populated it, so every guard reading it short-circuited to "allow".
456
+ Per ADR-0049 enforce-or-remove, the shape is deleted and the three consumers
457
+ now use the server-resolved `/auth/me/permissions` channel
458
+ (`usePermissions().checkField`) — the same channel ObjectForm/ModalForm/ListView
459
+ already enforce:
460
+
461
+ - **ImportWizard target fields (app-shell `ObjectView`)**: the importable
462
+ field set (and thus the downloadable CSV template's columns) now drops
463
+ fields the caller cannot edit, instead of offering columns the server's
464
+ FLS write gate would 403.
465
+ - **ObjectGrid auto-derived columns**: columns the caller cannot read are
466
+ dropped (same gate ListView applies), instead of a dead schema-shape check.
467
+ - **ObjectForm**: the redundant dead guard in field generation is removed;
468
+ the existing `applyFieldPerms` gate remains the real enforcement point.
469
+
470
+ BREAKING CHANGE: `@object-ui/types` field definitions no longer accept a
471
+ `permissions` key. It never carried data at runtime; consumers needing
472
+ per-caller field-level permissions must use `@object-ui/permissions`
473
+ (`MePermissionsProvider` + `useFieldPermissions`/`checkField`).
474
+
475
+ - dc7a798: fix(plugin-grid,plugin-form,plugin-designer,cli,vscode-extension): type-check the last five unchecked packages, and fix the two runtime bugs that hid there (#2919)
476
+
477
+ Closes the remaining `DEBT` entries from the #2911 sweep. Each package gains
478
+ `"type-check": "tsc --noEmit"` and loses its entry in
479
+ `scripts/check-type-check-coverage.mjs`; coverage goes 36 -> 41 of 45 and
480
+ outstanding errors 25 -> 5 (only #2916 `plugin-view` and #2918 `layout` remain).
481
+
482
+ **Two of these were real bugs, not just type noise.**
483
+
484
+ `@object-ui/cli` — `objectui validate` could never report a validation failure.
485
+ `ZodError.errors` was removed in Zod 4 (the repo is on 4.4.3), so `.errors` read
486
+ `undefined` and `.forEach` threw a `TypeError` that the enclosing `catch`
487
+ reported as `✗ Error reading or parsing schema file: Cannot read properties of
488
+ undefined` — swallowing the very errors the command exists to print. Now reads
489
+ `.issues`. Verified against the built CLI: an invalid schema now prints
490
+ `1. Invalid input / Code: invalid_union` and exits 1.
491
+
492
+ `@object-ui/plugin-grid` — grouping a grid by a boolean column showed the raw
493
+ i18n key. `t('grid.booleanTrue', 'Yes')` asked for a key present in neither
494
+ `GRID_DEFAULT_TRANSLATIONS` nor any locale bundle, and passed the English
495
+ fallback as a bare second argument — which `createSafeTranslation`'s no-provider
496
+ translator reads as an _options object_, so the fallback never applied and the
497
+ header rendered the literal `grid.booleanTrue`. Switched to the `grid.yes` /
498
+ `grid.no` keys the boolean cell renderer (`ObjectGrid.tsx`) and
499
+ `BulkActionDialog` already use, with the fallback passed as `defaultValue`.
500
+ Covered by a new regression test, confirmed to fail against the old code.
501
+
502
+ The rest are type-only corrections that preserve runtime behaviour exactly:
503
+
504
+ - **plugin-grid** `importParsers.ts` — `scorePair`'s `score`/`reason` moved into
505
+ one `best` record. They were captured `let`s mutated only inside the `bump`
506
+ closure, which TypeScript's control-flow analysis does not track, so it still
507
+ believed `reason` was `'none'` at the type gate and flagged the comparisons as
508
+ non-overlapping (TS2367). The gate — which stops a text column being mapped
509
+ onto a number field — is unchanged; its two dedicated tests still pass.
510
+ - **plugin-form** — `SectionFieldsContext.fieldLabel` now requires `fallback`,
511
+ matching the `useSafeFieldLabel` producer in `@object-ui/i18n` (an omitted
512
+ fallback could not satisfy the `=> string` return, and all four call sites
513
+ already pass one). This one signature cleared six errors.
514
+ `MasterDetailFormSchema.recordId` widens to `string | number`, matching
515
+ `ObjectFormSchema` and the five envelopes that forward straight into it;
516
+ it is narrowed with `String()` only at the batch-transaction boundary, whose
517
+ `BatchTransactionOperation.id` is a string by protocol (the `isEdit` guard
518
+ already proves it non-null there). `deriveMasterDetail`'s column sort gets an
519
+ explicit `fillPriority` helper — `GridColumn.type` is optional, and a column
520
+ without one keeps sorting at priority 5 exactly as the old
521
+ `TYPE_FILL_PRIORITY[undefined] ?? 5` lookup put it.
522
+ - **plugin-designer** — unused `index` parameter prefixed `_`, matching the
523
+ `_entry` beside it.
524
+ - **cli** — a stale `@ts-expect-error` removed; `viteConfig` is typed `any`, so
525
+ the line it guarded had stopped erroring.
526
+ - **vscode-extension** (`object-ui`) — migrated off `moduleResolution: "node"`,
527
+ which is deprecated and stops working in TypeScript 7, to `node16` paired with
528
+ `module: "node16"` (the package has no `"type": "module"`, so node16 resolves
529
+ it as the CommonJS that tsup emits, and it gains the `exports`-map awareness
530
+ node10 lacks). Its error count was under-reported as 1: that TS5107 config
531
+ error masked four more. The package uses `console`/`Buffer` but sets
532
+ `lib: ["ES2020"]` with no DOM and never declared `@types/node` — added, with an
533
+ explicit `types: ["node", "vscode"]`.
534
+
535
+ Also: `plugin-grid`, `plugin-form` and `plugin-designer` gain the `baseUrl` +
536
+ `paths` override their type-checked plugin peers already carry, and `cli` an
537
+ empty `paths`. Without it the inherited root `paths` point `@object-ui/*` at
538
+ sibling `src/`, which is outside each project's `rootDir` and produces the ~104
539
+ spurious TS6059 errors noted in #2915; workspace deps instead resolve through
540
+ node_modules to built `.d.ts`, which `type-check`'s `dependsOn: ["^build"]`
541
+ guarantees exist.
542
+
543
+ Verified the gate genuinely covers all five rather than trusting the green:
544
+ injecting a type error into each package makes `pnpm type-check --filter <pkg>`
545
+ fail, which was impossible before this change.
546
+
547
+ - Updated dependencies [7b21891]
548
+ - Updated dependencies [0b3be01]
549
+ - Updated dependencies [3c4d935]
550
+ - Updated dependencies [4b1ed7d]
551
+ - Updated dependencies [4b60d2d]
552
+ - Updated dependencies [952b978]
553
+ - Updated dependencies [de5e40c]
554
+ - Updated dependencies [1a03af6]
555
+ - Updated dependencies [3e886eb]
556
+ - Updated dependencies [cfc675e]
557
+ - Updated dependencies [20df08c]
558
+ - Updated dependencies [1767124]
559
+ - Updated dependencies [8ecf5a6]
560
+ - Updated dependencies [af705b9]
561
+ - Updated dependencies [0502a7c]
562
+ - Updated dependencies [7b35e4b]
563
+ - Updated dependencies [8fb1295]
564
+ - Updated dependencies [e16ed2d]
565
+ - Updated dependencies [c6fd752]
566
+ - Updated dependencies [f9bbddb]
567
+ - Updated dependencies [dfd3705]
568
+ - Updated dependencies [c77108c]
569
+ - Updated dependencies [2735de6]
570
+ - Updated dependencies [697cda4]
571
+ - Updated dependencies [c19ac11]
572
+ - Updated dependencies [6dee2cb]
573
+ - Updated dependencies [e05f052]
574
+ - Updated dependencies [0502a7c]
575
+ - Updated dependencies [faad45e]
576
+ - Updated dependencies [09c6a17]
577
+ - Updated dependencies [c7cff19]
578
+ - Updated dependencies [ba73a02]
579
+ - Updated dependencies [cd09a7b]
580
+ - Updated dependencies [f1abf0e]
581
+ - Updated dependencies [f05b84e]
582
+ - Updated dependencies [9b4b952]
583
+ - Updated dependencies [341bfb5]
584
+ - Updated dependencies [2f947e4]
585
+ - Updated dependencies [7d46648]
586
+ - Updated dependencies [9b53d72]
587
+ - Updated dependencies [bb4aa25]
588
+ - Updated dependencies [75f1cdf]
589
+ - Updated dependencies [662bdf9]
590
+ - Updated dependencies [059a052]
591
+ - Updated dependencies [53642d4]
592
+ - Updated dependencies [8aae006]
593
+ - Updated dependencies [c6cfdf1]
594
+ - Updated dependencies [d147a13]
595
+ - Updated dependencies [c6aaed8]
596
+ - Updated dependencies [263f885]
597
+ - Updated dependencies [dc334da]
598
+ - @object-ui/components@17.0.0
599
+ - @object-ui/i18n@17.0.0
600
+ - @object-ui/fields@17.0.0
601
+ - @object-ui/react@17.0.0
602
+ - @object-ui/types@17.0.0
603
+ - @object-ui/core@17.0.0
604
+ - @object-ui/permissions@17.0.0
605
+
606
+ ## 16.1.0
607
+
608
+ ### Minor Changes
609
+
610
+ - 62b9ab5: feat(data): unify master-detail saves behind `DataSource.batchTransaction`, isolate the non-atomic fallback in the adapter (#2679)
611
+
612
+ Master-detail saves (`MasterDetailForm`, `LineItemsPanel`) now always persist
613
+ through `dataSource.batchTransaction(operations)` — one ordered cross-object
614
+ operation list, with `{ $ref: <op index> }` linking a child to a parent created
615
+ in the same batch. The form no longer contains any client-side orchestration or
616
+ best-effort compensation-delete; that atomicity anti-pattern is gone from the UI
617
+ layer (framework #1604 / framework ADR-0034 item 4).
618
+
619
+ - **`@object-ui/types`** — `batchTransaction?` is now a first-class (optional)
620
+ method on the `DataSource` contract, typed via `BatchTransactionOperation` /
621
+ `BatchRef`. Replaces the previous `(dataSource as any).batchTransaction`
622
+ method-sniffing.
623
+ - **`@object-ui/core`** — new `emulateBatchTransaction(dataSource, operations)`
624
+ (sequential writes, `$ref` resolution, best-effort reverse-order compensation)
625
+ and `runBatchTransaction(dataSource, operations)` (prefers the adapter's method,
626
+ emulates otherwise). `ApiDataSource` / `ValueDataSource` implement
627
+ `batchTransaction` via the emulation.
628
+ - **`@object-ui/data-objectstack`** — `ObjectStackAdapter.batchTransaction` uses
629
+ the server's atomic `POST /api/v1/batch`, prefers the typed
630
+ `client.data.batchTransaction` SDK method when the installed client exposes it,
631
+ and degrades to the client-side emulation ONLY when the endpoint is missing
632
+ (404/405) or the runtime can't do transactions (501). Real errors (400/401/403/
633
+ 409/500) still surface. This is the isolated, tested home of the non-atomic
634
+ fallback.
635
+ - **`@object-ui/plugin-form`** — removed `applyDetail` / `createMany` /
636
+ `ApplyDetailResult` from `masterDetailTx.ts`; `MasterDetailForm` and
637
+ `LineItemsPanel` build ops and call `runBatchTransaction`. `LineItemsPanel`
638
+ saves are now atomic on a capable backend, with the rollup folded into the same
639
+ batch.
640
+
641
+ No behavior change on a current ObjectStack backend (it has `/api/v1/batch`);
642
+ older/limited backends keep a working — now clearly non-atomic — save path.
643
+
644
+ ### Patch Changes
645
+
646
+ - 7cf4051: chore(deps): align every `@objectstack/*` dependency to `^16.0.0-rc.0`
647
+
648
+ Bumps `@objectstack/spec` / `client` / `formula` / `lint` from `^15.1.1` to the
649
+ `16.0.0-rc.0` pre-release across the workspace (root + `apps/console` +
650
+ `apps/site` + all consuming packages). ObjectUI's own packages are already on
651
+ major 16, so this closes the 15↔16 skew between ObjectUI and the `@objectstack`
652
+ contract libraries (which publish in lockstep with `spec`).
653
+
654
+ This is a dependency alignment, not a behavioral migration: the full workspace
655
+ build (43/43) and the `@objectstack`-consuming package test suites
656
+ (`core` / `app-shell` / `data-objectstack` / `plugin-form` / `types`) are green
657
+ against `16.0.0-rc.0` with no source changes required.
658
+
659
+ Practical effect: `@objectstack/client@16.0.0-rc.0` now ships
660
+ `data.batchTransaction` (framework #3271), so `ObjectStackAdapter`'s feature
661
+ detect (`typeof client.data.batchTransaction === 'function'`) routes
662
+ master-detail cross-object saves through the typed SDK method instead of the
663
+ raw `fetch('/api/v1/batch')` fallback — realizing the "verify SDK path" half of
664
+ #2694. The raw-fetch branch stays as a defensive fallback (removal tracked in
665
+ #2694).
666
+
667
+ - 0a3710b: **Finish the `managedBy` / `userActions` de-dup — one parser for the override shape (completes objectui#2712, framework#3343).** #2712 consolidated the bucket _union_ + affordance _set_ mirrors but left four surfaces still parsing the `userActions.{create,edit,delete}` override shape by hand. They now all route through the shared `@object-ui/core` policy, so no package re-implements the boolean / #2614-object-form parse locally.
668
+
669
+ - **`@object-ui/core`** promotes the internal `normalizeOverride` to the exported **`normalizeUserAction(v, base)`** (the one parser) and adds **`userActionPredicates(v)`** for per-record CEL predicate extraction.
670
+ - **`app-shell/utils/managedByEmptyState.ts`** — the writable-`system` create check and its local `EmptyStateUserActions` interface are replaced by `resolveCrudAffordances({ managedBy, userActions }).create`.
671
+ - **`plugin-grid/rowCrudAffordances.ts`** — the local `isOptedOut` / `predicatesOf` helpers (and duplicated `RowCrudUserAction` / `RowCrudPredicates` types) fold into `normalizeUserAction`; the historical type names stay re-exported for compat.
672
+ - **`plugin-detail/RelatedList.tsx`** — its inline `predicatesOf` fold into `userActionPredicates`.
673
+ - **`plugin-form/ObjectForm.tsx`** — the hand-rolled `managedBy !== 'platform'` blanket lock + `userActions` unlock is replaced by the resolved affordance for the current mode (`edit` / `create`), the **same** `resolveCrudAffordances` contract the detail (`isObjectInlineEditable`) and grid surfaces use.
674
+
675
+ Behavior-preserving for `platform` / `system` / `append-only` / `better-auth`, with one deliberate alignment: an admin-editable **`config`**-bucket object (e.g. `sys_webhook`, `sys_permission_set`) is now editable in `ObjectForm` — it was previously over-locked as "non-`platform`", while detail/grid already treated it as editable (`config` resolves `edit: true`). New unit coverage for the shared parser and the config / create-mode form gate; all existing affordance/edit-gate tests stay green.
676
+
677
+ - Updated dependencies [0318118]
678
+ - Updated dependencies [1c8935a]
679
+ - Updated dependencies [af1b0db]
680
+ - Updated dependencies [8b8b744]
681
+ - Updated dependencies [7cf4051]
682
+ - Updated dependencies [803558e]
683
+ - Updated dependencies [aefcf39]
684
+ - Updated dependencies [2e7d7f0]
685
+ - Updated dependencies [ef14f69]
686
+ - Updated dependencies [94d4876]
687
+ - Updated dependencies [1100a8b]
688
+ - Updated dependencies [7abe4cd]
689
+ - Updated dependencies [69fa5d1]
690
+ - Updated dependencies [549c67d]
691
+ - Updated dependencies [ebe6494]
692
+ - Updated dependencies [2b17339]
693
+ - Updated dependencies [31b77d4]
694
+ - Updated dependencies [6d4fbe6]
695
+ - Updated dependencies [0a3710b]
696
+ - Updated dependencies [f80aaf2]
697
+ - Updated dependencies [62b9ab5]
698
+ - Updated dependencies [14cb729]
699
+ - Updated dependencies [1629313]
700
+ - Updated dependencies [29c6040]
701
+ - Updated dependencies [faebac3]
702
+ - Updated dependencies [2331ac9]
703
+ - Updated dependencies [199fa83]
704
+ - Updated dependencies [eee4ded]
705
+ - Updated dependencies [3b2e4d9]
706
+ - @object-ui/fields@16.1.0
707
+ - @object-ui/i18n@16.1.0
708
+ - @object-ui/core@16.1.0
709
+ - @object-ui/types@16.1.0
710
+ - @object-ui/react@16.1.0
711
+ - @object-ui/components@16.1.0
712
+ - @object-ui/permissions@16.1.0
713
+
3
714
  ## 16.0.0
4
715
 
5
716
  ### Minor Changes
@@ -793,7 +1504,7 @@ parentData, details)` — emits a parent `update` op (index 0) then diffs each
793
1504
  `type:'tabbed'` form view now renders tabbed in the modal too — not just on the
794
1505
  full-page route (#1762). Non-breaking; `FormView.type` enum unchanged.
795
1506
 
796
- Refs objectstack-ai/framework#1890, ADR-0050
1507
+ Refs objectstack-ai/objectstack#1890, ADR-0050
797
1508
 
798
1509
  - 650bd1f: fix(forms/dashboard/related-list): four business-facing rendering fixes found while QA-ing a showcase workspace
799
1510