@object-ui/fields 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 (71) hide show
  1. package/CHANGELOG.md +630 -0
  2. package/README.md +45 -7
  3. package/dist/FieldEditWidget.d.ts +7 -0
  4. package/dist/field-type-alias.d.ts +16 -0
  5. package/dist/index.cjs +5 -5
  6. package/dist/index.d.ts +35 -11
  7. package/dist/index.js +2241 -1853
  8. package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
  9. package/dist/widgets/CheckboxesField.d.ts +8 -1
  10. package/dist/widgets/FileField.d.ts +1 -1
  11. package/dist/widgets/FilterConditionField.d.ts +32 -0
  12. package/dist/widgets/ImageField.d.ts +1 -1
  13. package/dist/widgets/ImageLightbox.d.ts +30 -0
  14. package/dist/widgets/ImageLightbox.test.d.ts +1 -0
  15. package/dist/widgets/LookupField.hydration.test.d.ts +1 -0
  16. package/dist/widgets/LookupField.idField.test.d.ts +1 -0
  17. package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
  18. package/dist/widgets/MultiSelectField.d.ts +10 -2
  19. package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
  20. package/dist/widgets/RadioField.d.ts +8 -1
  21. package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
  22. package/dist/widgets/SelectField.d.ts +13 -9
  23. package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +8 -0
  24. package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +8 -0
  25. package/dist/widgets/deriveLookupColumns.d.ts +1 -0
  26. package/dist/widgets/file-value.d.ts +112 -0
  27. package/dist/widgets/file-value.test.d.ts +1 -0
  28. package/dist/widgets/types.d.ts +6 -0
  29. package/dist/widgets/useCascadingOptions.d.ts +18 -0
  30. package/dist/widgets/useFieldTranslation.d.ts +2 -0
  31. package/dist/widgets/useUploadingSignal.d.ts +9 -0
  32. package/dist/widgets/useUploadingSignal.test.d.ts +1 -0
  33. package/package.json +11 -10
  34. package/src/FieldEditWidget.test.ts +74 -2
  35. package/src/FieldEditWidget.tsx +44 -4
  36. package/src/field-type-alias.ts +100 -0
  37. package/src/field-type-coverage.test.ts +3 -1
  38. package/src/index.tsx +137 -135
  39. package/src/standard-widgets.test.tsx +44 -0
  40. package/src/widgets/CheckboxesField.cascade.test.tsx +143 -0
  41. package/src/widgets/CheckboxesField.tsx +77 -11
  42. package/src/widgets/FileField.tsx +91 -52
  43. package/src/widgets/FilterConditionField.tsx +109 -9
  44. package/src/widgets/ImageField.tsx +106 -65
  45. package/src/widgets/ImageLightbox.test.tsx +52 -0
  46. package/src/widgets/ImageLightbox.tsx +108 -0
  47. package/src/widgets/LookupField.hydration.test.tsx +136 -0
  48. package/src/widgets/LookupField.idField.test.tsx +61 -0
  49. package/src/widgets/LookupField.tsx +106 -9
  50. package/src/widgets/MultiSelectField.cascade.test.tsx +143 -0
  51. package/src/widgets/MultiSelectField.tsx +78 -11
  52. package/src/widgets/ObjectRefField.tsx +9 -4
  53. package/src/widgets/PeoplePicker.test.tsx +20 -0
  54. package/src/widgets/PeoplePicker.tsx +15 -3
  55. package/src/widgets/RadioField.cascade.test.tsx +141 -0
  56. package/src/widgets/RadioField.tsx +69 -9
  57. package/src/widgets/RecipientPickerField.test.tsx +157 -0
  58. package/src/widgets/RecipientPickerField.tsx +36 -16
  59. package/src/widgets/SelectField.tsx +33 -31
  60. package/src/widgets/TextAreaField.tsx +8 -3
  61. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +66 -0
  62. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +142 -0
  63. package/src/widgets/deriveLookupColumns.test.ts +13 -0
  64. package/src/widgets/deriveLookupColumns.ts +5 -15
  65. package/src/widgets/file-value.test.ts +230 -0
  66. package/src/widgets/file-value.ts +230 -0
  67. package/src/widgets/types.ts +6 -0
  68. package/src/widgets/useCascadingOptions.ts +46 -0
  69. package/src/widgets/useFieldTranslation.ts +30 -0
  70. package/src/widgets/useUploadingSignal.test.tsx +62 -0
  71. package/src/widgets/useUploadingSignal.ts +20 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,635 @@
1
1
  # @object-ui/fields
2
2
 
3
+ ## 17.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f8a95e5: fix(fields): the sharing-criteria builder stops calling an empty criteria "All records" (objectstack#3896)
8
+
9
+ `FilterConditionField` renders `sys_sharing_rule.criteria_json`. With no
10
+ criteria it displayed **"All records"**, and `filterGroupToMongo` carried a
11
+ matching `// empty = match all` comment. That was describing a bug as a
12
+ feature: a sharing rule with no predicate was stored as `criteria_json: null`
13
+ and evaluated as `find(object, { filter: {} })` under the system context —
14
+ every record of the object, granted to the recipient. `SharingRuleSchema` had
15
+ always forbidden the shape ("never seeded as a permissive match-all",
16
+ ADR-0049); the REST and data-API entries just never checked.
17
+
18
+ objectstack#3896 closes those entries: the server now refuses to save a rule
19
+ whose criteria would match everything, and one already stored shares nothing.
20
+ This is the renderer catching up.
21
+
22
+ - **The empty read-only state now says the rule shares nothing**, in
23
+ `destructive` styling — key renamed `fields.filterCondition.allRecords` →
24
+ `fields.filterCondition.noCriteria`, retranslated across all ten locales.
25
+ Nothing else read the old key.
26
+ - **A new `fields.filterCondition.criteriaRequired` hint** renders under the
27
+ builder (and the JSON editor) while the criteria is empty. The server's
28
+ rejection is precise but only arrives as a toast _after_ Save; this says it
29
+ while the admin is still looking at the empty builder.
30
+ - **`isMatchAllCriteria` is exported** — a client-side mirror of the server
31
+ predicate covering `{}`, `[]`, and the vacuous combinators (`{ $and: [] }`,
32
+ `{ $or: [{}] }`), conservative in the same direction. The server stays
33
+ authoritative; this only decides whether to show the hint.
34
+
35
+ Unparsable JSON keeps its own `invalidJson` message and does **not** also
36
+ collect the empty-criteria hint.
37
+
38
+ Note for anyone wiring this end-to-end: the Criteria field is not marked
39
+ `required` in the object metadata, deliberately — `sys_sharing_rule.criteria_json`
40
+ is nullable in deployed tenants, so `required: true` would only produce a
41
+ destructive `NOT NULL` migration that those nulls block. The invariant lives in
42
+ the server's write guards; this change makes the UI stop contradicting it.
43
+
44
+ ### Patch Changes
45
+
46
+ - aecc934: fix(fields): PeoplePicker's keyboard cursor can no longer be eaten by a late reset
47
+
48
+ The cursor reset on new results lived in a `useEffect`. Effects flush
49
+ asynchronously after the render that delivered the records — so a reset queued
50
+ by their arrival could land AFTER a subsequent ArrowDown and wipe the just-set
51
+ cursor. That was the residual ArrowDown→Enter flake in
52
+ `PeoplePicker.test.tsx` (the earlier signature-keyed fix closed the
53
+ too-often resets, not the too-late one), and a real fast-fingers UX bug: rows
54
+ appear, the user presses ArrowDown, the highlight vanishes.
55
+
56
+ The reset now runs in the render phase (the "adjusting state during render"
57
+ pattern), in the same render that shows the new rows — by the time a row is
58
+ visible, the reset has already happened, so it can never race a keypress.
59
+ Semantics unchanged and now pinned by a test: a replaced result set does not
60
+ inherit the previous set's cursor.
61
+
62
+ - 7f23cd0: fix(form): a numeric/boolean select option survives selection with its type intact — #3090
63
+
64
+ `SelectOptionSchema.value` has accepted `string | number | boolean` for as
65
+ long as it has existed, but the Radix controls underneath speak strings:
66
+ picking `{ value: 2 }` silently submitted `"2"` — a wrong-typed write into a
67
+ number field that nothing on the client ever reported. (Display half-worked:
68
+ a numeric default matched its numeric item; only SELECTION morphed the type.)
69
+
70
+ The renderers now stringify on the way into the control and map the selection
71
+ back to the AUTHORED option value on the way out (`matchOptionValue`), across
72
+ the in-form select, the standalone `type: 'select'` component, and the
73
+ standalone `type: 'radio-group'` component. The TS types stop lying to match:
74
+ `SelectOption.value` / `RadioOption.value` and the corresponding
75
+ `value`/`defaultValue`/`onChange` channels widen to what the zod schemas
76
+ always accepted — a call site treating `option.value` as `string` is now a
77
+ compile error pointing at a real latent crash, not a false comfort.
78
+
79
+ The ripple the widening named, handled at each boundary: `@object-ui/core`'s
80
+ `OptionLike.value` widens (the option engines compare by identity, so values
81
+ flow opaquely; the option-lint's CEL-literal domain stringifies at its
82
+ boundary), and the multi-value field widgets (checkboxes / multiselect /
83
+ radio) stringify at theirs — multi-value fields store string arrays.
84
+
85
+ Round-trip pinned by real Radix interactions in jsdom: the in-form select
86
+ submits `2` (number), the standalone select hands its handler `false`
87
+ (boolean).
88
+
89
+ - Updated dependencies [62311b6]
90
+ - Updated dependencies [fc0272a]
91
+ - Updated dependencies [9e7349e]
92
+ - Updated dependencies [8864971]
93
+ - Updated dependencies [1cf0de7]
94
+ - Updated dependencies [752e18f]
95
+ - Updated dependencies [c785740]
96
+ - Updated dependencies [b41f401]
97
+ - Updated dependencies [19e9fa0]
98
+ - Updated dependencies [d61efd1]
99
+ - Updated dependencies [95b7214]
100
+ - Updated dependencies [7d9734d]
101
+ - Updated dependencies [6ae818e]
102
+ - Updated dependencies [9eb932b]
103
+ - Updated dependencies [746dd00]
104
+ - Updated dependencies [aebfa4f]
105
+ - Updated dependencies [38ca8be]
106
+ - Updated dependencies [3cb9646]
107
+ - Updated dependencies [68ef584]
108
+ - Updated dependencies [4952edf]
109
+ - Updated dependencies [7f0252e]
110
+ - Updated dependencies [c4d7b20]
111
+ - Updated dependencies [c769d3d]
112
+ - Updated dependencies [7639a61]
113
+ - Updated dependencies [94e63ef]
114
+ - Updated dependencies [c735bf7]
115
+ - Updated dependencies [02aef0c]
116
+ - Updated dependencies [6f29aa5]
117
+ - Updated dependencies [d21794c]
118
+ - Updated dependencies [c4db402]
119
+ - Updated dependencies [5319bf1]
120
+ - Updated dependencies [49e5671]
121
+ - Updated dependencies [9a04d25]
122
+ - Updated dependencies [b5b97e2]
123
+ - Updated dependencies [f59f2c1]
124
+ - Updated dependencies [07de839]
125
+ - Updated dependencies [2a40b5e]
126
+ - Updated dependencies [df613fa]
127
+ - Updated dependencies [4874117]
128
+ - Updated dependencies [ad0183a]
129
+ - Updated dependencies [ce08d55]
130
+ - Updated dependencies [eb4b740]
131
+ - Updated dependencies [5b084eb]
132
+ - Updated dependencies [aa1240a]
133
+ - Updated dependencies [2374a49]
134
+ - Updated dependencies [390c071]
135
+ - Updated dependencies [d10f526]
136
+ - Updated dependencies [2d5d594]
137
+ - Updated dependencies [ea7f477]
138
+ - Updated dependencies [379728f]
139
+ - Updated dependencies [7f23cd0]
140
+ - Updated dependencies [0ded602]
141
+ - Updated dependencies [24e0e0a]
142
+ - Updated dependencies [f8a95e5]
143
+ - Updated dependencies [3a6cf24]
144
+ - Updated dependencies [aa35561]
145
+ - Updated dependencies [03bd53b]
146
+ - Updated dependencies [3c1f321]
147
+ - Updated dependencies [a045a32]
148
+ - Updated dependencies [912496d]
149
+ - Updated dependencies [80edbd4]
150
+ - Updated dependencies [9867281]
151
+ - @object-ui/core@17.1.0
152
+ - @object-ui/components@17.1.0
153
+ - @object-ui/react@17.1.0
154
+ - @object-ui/types@17.1.0
155
+ - @object-ui/i18n@17.1.0
156
+ - @object-ui/providers@17.1.0
157
+
158
+ ## 17.0.0
159
+
160
+ ### Minor Changes
161
+
162
+ - 697cda4: feat(fields): adopt the file-as-reference value shape (ObjectStack ADR-0104 D3 wave 2)
163
+
164
+ A `file`/`image` field value now reaches the UI in one of three forms, and the
165
+ rules for reading them live in one place — `@object-ui/fields`' new
166
+ `file-value` module — instead of being re-derived in each widget:
167
+
168
+ 1. **Reference** — a bare `sys_file` id string, what the backend stores once
169
+ file-as-reference is adopted.
170
+ 2. **Expanded** — `{ id, name, size, mimeType, url }`, what the read path
171
+ returns after resolving a reference.
172
+ 3. **Legacy inline blob** — `{ file_id?, name, original_name, size, mime_type,
173
+ url }`, the pre-reference shape this package used to build itself.
174
+
175
+ **The casing split is the bug this fixes.** The expanded form carries
176
+ `mimeType`; the legacy blob carries `mime_type`. `FileField`, `FileCell` and
177
+ `ImageField` all read only `mime_type`, so the moment a backend starts returning
178
+ the expanded form they stop recognising images — thumbnails silently degrade to
179
+ a generic file icon, with nothing pointing at a value shape as the cause.
180
+ `readFileValue()` accepts both.
181
+
182
+ **Uploads now submit the reference form** — the bare `sys_file` id — when the
183
+ upload adapter surfaced one, falling back to the legacy blob when it did not
184
+ (the object-URL fallback adapter, or a backend predating file-as-reference). The
185
+ same build therefore works against both. Action params already POSTed a bare
186
+ fileId; record field values now use the same contract, and
187
+ `serializeParamValues` shares the `fileIdOf()` extractor so the two surfaces
188
+ cannot drift on what counts as an id.
189
+
190
+ Because a bare id carries no name or URL, each widget remembers the display
191
+ details of files it just uploaded, keyed by id, so an upload renders immediately
192
+ rather than showing a bare token until the next read enriches it.
193
+
194
+ ### Patch Changes
195
+
196
+ - 4b1ed7d: feat(app-shell): approval approver values become record lookups (framework #3508)
197
+
198
+ - The flow designer's approver `Value` cell now sources directory kinds from DATA
199
+ records instead of the metadata registry: `user` / `team` / `department` / `position`
200
+ render a single-select record lookup (`LookupField` over `sys_user` / `sys_team` /
201
+ `sys_business_unit` / `sys_position` via the DataSource adapter), with a manual-entry
202
+ escape hatch and a plain free-text fallback when no adapter is available (offline
203
+ preview). `position` commits the machine name; the others commit the record id —
204
+ matching the approval engine's resolution semantics.
205
+ - `org-membership-level` is now a strict select (owner/admin/member); a stored
206
+ out-of-enum value renders flagged instead of being blanked.
207
+ - `manager` renders as an auto-resolved (disabled) cell; `queue` is no longer offered
208
+ for new approver rows and stored queue rows carry a "not supported by the runtime"
209
+ warning.
210
+ - `@object-ui/fields`: `LookupField` hydrates the selected label through `id_field`
211
+ when it is not the primary id (e.g. `id_field: 'name'`), instead of always calling
212
+ `findOne` with the primary id.
213
+
214
+ - 3e886eb: fix(i18n): localize FileField upload widget + approvals snapshot field labels
215
+
216
+ - `FileField` (the shared upload widget) hard-coded every visible string
217
+ ("Drag & drop files here", "or click to browse", "Take photo", "Uploading…",
218
+ size/upload validation messages, …). They now route through
219
+ `useObjectTranslation` with new `fields.file.*` keys, translated across all
220
+ 10 locale bundles. This is why the approvals Approve/Reject dialog's
221
+ attachment dropzone was English in a Chinese console.
222
+ - The approvals inbox record-snapshot summary title-cased raw machine keys
223
+ instead of the target object's field labels. It now consumes the
224
+ server-sent `payload_labels` in `payloadSummary`/`decisionAmountEntry`,
225
+ falling back to the prettified key when absent; `approvalsApi`'s row type
226
+ gains `payload_labels`.
227
+
228
+ - faad45e: fix(fields): render `image` fields consistently and add click-to-zoom (#2836)
229
+
230
+ An `image` field rendered differently — and wrongly — on three surfaces:
231
+
232
+ - **Edit form showed broken thumbnails.** A record read back its `image` value
233
+ as a bare `sys_file` id (the reference form), but `readFileValue` returned an
234
+ id with no URL — the comment assumed the read path expands it, which the
235
+ edit-form data path does not. The result was `<img src="">`. `file-value` now
236
+ derives the stable download URL (`/api/v1/storage/files/:id`, which
237
+ 302-redirects to a signed URL and works directly as `<img src>`) for a bare
238
+ id or an id-only object, so every widget and cell renderer resolves one.
239
+ - **Inline edit leaked the raw storage URL.** `InlineFieldInput` had no branch
240
+ for file-backed types and fell through to a plain text input showing
241
+ `/api/v1/storage/files/…`. It now renders the same upload widgets the form
242
+ uses (`image`/`avatar`/`signature`/`file`/`video`/`audio`).
243
+ - **Hard-coded English.** `ImageField`'s upload/crop/remove/alt strings now go
244
+ through `t('fields.image.*')` (en + zh added).
245
+
246
+ Also adds an `ImageLightbox` — click a read-only thumbnail (detail or list cell)
247
+ to open a full-screen preview; multiple images get prev/next navigation, a
248
+ position counter and arrow-key support, a single image just the image. In a
249
+ grid cell the click is `stopPropagation`-guarded so enlarging doesn't also open
250
+ the row.
251
+
252
+ - 341bfb5: fix: read spec-canonical keys for dashboard header title and field length rules
253
+
254
+ Two naming-drift closeouts (framework#1878 / framework#1891):
255
+
256
+ - `DashboardRenderer` header now falls back to the spec-canonical `label` when
257
+ the legacy `title` is absent (mirrors the `DashboardGridLayout` fallback from
258
+ #2666) — a spec-compliant dashboard gets its header title.
259
+ - Field validation rules now read the spec-canonical camelCase
260
+ `minLength`/`maxLength` (what the server record-validator enforces) with the
261
+ legacy snake_case `min_length`/`max_length` kept as fallback — authored
262
+ length constraints reach the client form.
263
+
264
+ - 2f947e4: fix(page,field): consume the spec's `type`/`label`/`maxLength` keys (framework#1878 §3 naming-drift recheck)
265
+
266
+ Three forward-drifts where objectui read a different key than the spec
267
+ declares, so authoring the documented key silently no-oped:
268
+
269
+ - **page `type` → `pageType`** (app-shell + components): `PageSchema` declares
270
+ the page KIND as `type`, but `PageRenderer` reads `schema.pageType` and fell
271
+ back to `'record'` — and nothing mapped between them. Every non-record page
272
+ (`home`/`app`/`list`/`utility`) rendered with the record max-width, a wrong
273
+ `data-page-type` attribute, and a suppressed header. `PageView` now passes
274
+ `pageType` alongside the SchemaNode discriminator `type`.
275
+ - **page `label` → `title`** (components): `PageSchema.label` is required but the
276
+ region renderer read only `title`. Now dual-reads `title ?? label`, mirroring
277
+ the fallback `DashboardRenderer` already uses. Coupled with the above — the
278
+ header is gated on `pageType !== 'record'`, so both were needed for a title to
279
+ appear.
280
+ - **field `maxLength`/`minLength`** (plugin-form + fields): validation already
281
+ dual-read these, but `ObjectForm`'s HTML-attribute pass and `TextAreaField`
282
+ read `max_length` only, so a spec-authored `maxLength` gave no browser cap and
283
+ no character counter. Both now dual-read, matching `buildValidationRules`.
284
+
285
+ Verified in the browser against the showcase: `capability_map` (`type: 'home'`)
286
+ now renders `data-page-type="home"`, the `home` max-width and its page title;
287
+ record pages are unchanged.
288
+
289
+ - 53642d4: fix(core,fields): a string `$orderby` is a clause, not a character array — and localize the sharing-rule widgets (objectstack#3821)
290
+
291
+ **The recipient picker listed nothing, ever.** `QueryParams['$orderby']` was
292
+ typed as `Record | string[] | SortObject[]`, so `queryParamsToRecord` sent any
293
+ non-array value through `Object.entries`. Handed the clause string `'name asc'`
294
+ — which callers do build by hand — it walked the string index by index and
295
+ emitted `$orderby=0 n,1 a,2 m,3 e,4 ,5 a,6 s,7 c`. The server sorted by columns
296
+ that don't exist and every row was filtered out, so
297
+ `sys_sharing_rule.recipient_id` rendered "No matches" for every recipient type
298
+ and no sharing rule could be created from the Console. `ObjectGrid` builds the
299
+ same shape from a schema-level `sort` in three places, so grids with a string
300
+ sort silently showed an empty table.
301
+
302
+ A string `$orderby` is now passed through verbatim (the server's OData
303
+ normalizer has always parsed `'name asc'`), and the type admits `string`.
304
+ `RecipientPickerField` additionally switched to the structured
305
+ `{ name: 'asc' }` form so it can't regress this way against any data source.
306
+
307
+ **The three sharing-rule authoring widgets never had translations.**
308
+ `ObjectRefField`, `RecipientPickerField` and `FilterConditionField` hardcoded
309
+ their English copy — a Chinese Console showed "Select an object", "Select a
310
+ user", "Search…", "No matches", "Edit as JSON". They now go through
311
+ `useFieldTranslation` like every other widget, with keys added under `fields.*`
312
+ in all ten locales.
313
+
314
+ The recipient placeholder was the interesting one: it read
315
+ `` `Select a ${recipientType.replace(/_/g,' ')}` ``, interpolating the enum
316
+ value into an English sentence — a shape no locale can translate. It is now a
317
+ per-type key (`fields.recipient.selectUser`, `…selectBusinessUnit`, …), so
318
+ "选择业务单元" and "Select a business unit" no longer have to share a structure.
319
+
320
+ **Editing a rule silently dropped its recipient.** The picker resets the stored
321
+ id when `recipient_type` changes, because an id valid for a user is meaningless
322
+ for a team. It treated the edit form's `'' → 'user'` hydration as such a change:
323
+ opening any saved rule blanked the recipient, and saving persisted the blank.
324
+ Only a non-empty predecessor now counts as a type switch.
325
+
326
+ **Building a filter submitted the surrounding form.** None of `FilterBuilder`'s
327
+ controls declared `type="button"`, and a bare `<button>` inside a `<form>`
328
+ defaults to `type="submit"`. Adding, removing or clearing a condition therefore
329
+ submitted the sharing-rule dialog — firing validation mid-edit, and on an
330
+ already-valid form saving the record before the admin was done.
331
+
332
+ **A rejected write showed the user raw server diagnostics.** The form rendered
333
+ `error.message` verbatim, so a sharing / RLS denial reached the dialog and the
334
+ toast as `FORBIDDEN: insufficient privileges to update showcase_private_note
335
+ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and the
336
+ record id to whoever hit it. Permission failures now render localized copy
337
+ (`form.noPermissionToSave`, added in all ten locales), with the server text kept
338
+ on the console for debugging; other failures still show the server's message,
339
+ which is the useful part, and fall back to `form.submitFailed` when there is
340
+ none — replacing the previously hardcoded English "An error occurred during
341
+ submission".
342
+
343
+ **The detail header offered "Edit" on records the user may only read.** Object
344
+ permissions can't express "this one record is read-only" — a read-only sharing
345
+ grant sits inside an object the user may otherwise edit — so the header showed
346
+ the primary Edit CTA, opened the form, and let the user retype a field before
347
+ the server rejected the save. `DetailView` now gates Edit / Delete on the
348
+ object-level check AND on the explain engine's record-grained verdict
349
+ (`POST /api/v1/security/explain` with a `recordId`, ADR-0090 D6 / ADR-0095 C2 —
350
+ the same pipeline the enforcement middleware runs, so button and server cannot
351
+ disagree). Explaining oneself needs no special permission. The probe is one
352
+ cached request per record, skipped entirely when the object-level check already
353
+ says no, and **fails open** on every uncertainty — an unanswered hint must never
354
+ be the reason a permitted user cannot act; the server stays the authority
355
+ (ADR-0057 D10).
356
+
357
+ **A long option rendered straight past the combobox border.** `Combobox`'s
358
+ trigger pinned itself to the component's `w-[200px]` default while the fields
359
+ around it ran the full form column, and the selected label was a bare text child
360
+ of a flex button — flex items need `truncate` AND `min-w-0` to clip, and it had
361
+ neither. So "成员 (showcase_project_membership)" in the object picker overflowed
362
+ the control and collided with the field beside it. The label now truncates, the
363
+ trigger can shrink, the dropdown matches the trigger's width instead of a
364
+ hardcoded 200px (a widened combobox used to clip its own options), and the two
365
+ sharing-rule pickers ask for `w-full` so they line up with every other input.
366
+
367
+ Hardens `evaluatePermission` while there: a role config carrying only
368
+ `fieldPermissions` (no `actions`) made `check()` throw a TypeError that
369
+ propagated out of the render. A permission check must not be able to crash a
370
+ view.
371
+
372
+ Browser-verified against the framework showcase Console in Chinese: object /
373
+ criteria / recipient copy is fully localized, the recipient dropdown lists real
374
+ users, business units and positions, a saved rule reopens with its recipient and
375
+ criteria intact, editing the filter no longer submits, and a rule created
376
+ end-to-end stores a real record id rather than free text. The criteria authored
377
+ in the builder is honored by the evaluator: `{"pinned":true}` on an owner-private
378
+ object granted the recipient exactly the matching records and nothing else.
379
+
380
+ - Updated dependencies [7b21891]
381
+ - Updated dependencies [0b3be01]
382
+ - Updated dependencies [3c4d935]
383
+ - Updated dependencies [4b60d2d]
384
+ - Updated dependencies [952b978]
385
+ - Updated dependencies [de5e40c]
386
+ - Updated dependencies [1a03af6]
387
+ - Updated dependencies [3e886eb]
388
+ - Updated dependencies [cfc675e]
389
+ - Updated dependencies [20df08c]
390
+ - Updated dependencies [1767124]
391
+ - Updated dependencies [8ecf5a6]
392
+ - Updated dependencies [af705b9]
393
+ - Updated dependencies [0502a7c]
394
+ - Updated dependencies [7b35e4b]
395
+ - Updated dependencies [8fb1295]
396
+ - Updated dependencies [e16ed2d]
397
+ - Updated dependencies [c6fd752]
398
+ - Updated dependencies [f9bbddb]
399
+ - Updated dependencies [dfd3705]
400
+ - Updated dependencies [c77108c]
401
+ - Updated dependencies [2735de6]
402
+ - Updated dependencies [c19ac11]
403
+ - Updated dependencies [6dee2cb]
404
+ - Updated dependencies [e05f052]
405
+ - Updated dependencies [0502a7c]
406
+ - Updated dependencies [faad45e]
407
+ - Updated dependencies [09c6a17]
408
+ - Updated dependencies [c7cff19]
409
+ - Updated dependencies [ba73a02]
410
+ - Updated dependencies [cd09a7b]
411
+ - Updated dependencies [f1abf0e]
412
+ - Updated dependencies [f05b84e]
413
+ - Updated dependencies [9b4b952]
414
+ - Updated dependencies [2f947e4]
415
+ - Updated dependencies [7d46648]
416
+ - Updated dependencies [9b53d72]
417
+ - Updated dependencies [bb4aa25]
418
+ - Updated dependencies [75f1cdf]
419
+ - Updated dependencies [662bdf9]
420
+ - Updated dependencies [059a052]
421
+ - Updated dependencies [53642d4]
422
+ - Updated dependencies [8aae006]
423
+ - Updated dependencies [c6cfdf1]
424
+ - Updated dependencies [d147a13]
425
+ - Updated dependencies [c6aaed8]
426
+ - Updated dependencies [263f885]
427
+ - Updated dependencies [dc334da]
428
+ - @object-ui/components@17.0.0
429
+ - @object-ui/i18n@17.0.0
430
+ - @object-ui/react@17.0.0
431
+ - @object-ui/types@17.0.0
432
+ - @object-ui/core@17.0.0
433
+ - @object-ui/providers@17.0.0
434
+
435
+ ## 16.1.0
436
+
437
+ ### Minor Changes
438
+
439
+ - 1c8935a: feat(app-shell): render ActionParamDialog params through the shared form field-widget renderer (ADR-0059, #2700)
440
+
441
+ `ActionParamDialog` no longer hand-rolls a per-type ternary chain (select /
442
+ lookup / textarea / number / boolean, everything else → text input). Every
443
+ declared action param now renders through the same `fieldWidgetMap` the object
444
+ form uses, so a param of ANY form-supported field type — `file`, `image`,
445
+ `richtext`, `markdown`, `color`, `address`, `code`, `date`, … — gets its real
446
+ widget, lazily loaded behind `Suspense`. Subsumes the single `file` branch ask
447
+ in #2698: `type: 'file'` params render the real `FileField` upload control via
448
+ the ambient `UploadProvider`, honoring `multiple`/`accept`/`maxSize`.
449
+
450
+ - `@object-ui/fields`: new exports `resolveFormWidgetType(type)` (widget-key
451
+ resolution incl. spec aliases, text fallback) and `getLazyFieldWidget(type)`
452
+ (per-type-cached `React.lazy` over the form's own widget loaders).
453
+ - `@object-ui/core`: `ActionParamDef` gains `accept`/`maxSize`; `multiple` is
454
+ now general widget config (was lookup-only).
455
+ - `@object-ui/app-shell`: new pure `paramToField()` adapter (param → field
456
+ shape) with a drift test pinning param support ⊇ form support (`FORM_FIELD_TYPES`),
457
+ mirroring the FieldEditWidget parity guard; `resolveActionParams()` inherits
458
+ `multiple`/`accept`/`maxSize` from the referenced field for every type.
459
+ `required` validation, `visible` CEL gating, helpText, error styling, and
460
+ value shapes for previously-supported types are unchanged.
461
+
462
+ - ef14f69: feat(fields): CheckboxesField per-option `visibleWhen` cascading + `dependsOn` gating (completes the option-widget parity set)
463
+
464
+ `checkboxes` was the last static-option widget still rendering `config.options`
465
+ raw — with no per-option `visibleWhen` filtering, `dependsOn` gating, or cascade
466
+ clear. It now matches `MultiSelectField` (its multi-value sibling), completing
467
+ the ADR-0058 parity across `select` / `multiselect` / `radio` / `checkboxes`.
468
+
469
+ - **`@object-ui/fields`**: `CheckboxesField` routes through the shared
470
+ `useCascadingOptions` hook — offered boxes narrow against the live record +
471
+ `current_user`, the control gates behind a "select the parent first" hint
472
+ while a `dependsOn` field is empty, and selections no longer offered are
473
+ pruned per-element from the array. Adds `checkboxes-empty-*` /
474
+ `checkboxes-option-*` testids.
475
+ - **`@object-ui/components`**: adds `checkboxes` to the form renderer's option
476
+ field sets (`CASCADE_OPTION_FIELD_TYPES`, the cross-field cascade-clear
477
+ effect, and the option pre-filter) so a `checkboxes` field is threaded
478
+ `dependentValues` and gated identically to the other option widgets.
479
+ - Tests: `CheckboxesField.cascade.test.tsx` mirrors `MultiSelectField.cascade.test.tsx`.
480
+
481
+ - 14cb729: feat(fields): MultiSelectField per-option `visibleWhen` cascading + `dependsOn` gating (parity with single select, #2715)
482
+
483
+ The multi-value chip picker now implements the same ADR-0058 option
484
+ resolution as the single `SelectField`, closing the gap #2709 opened when a
485
+ `select` + `multiple` (and the `multiselect` type) started delegating to it.
486
+
487
+ - Extracted `useCascadingOptions` — the shared hook that resolves per-option
488
+ `visibleWhen` filtering, `dependsOn` gating, and the live `dependentValues` +
489
+ predicate-scope wiring — and routed both `SingleSelectField` and
490
+ `MultiSelectField` through it (no duplicated resolver).
491
+ - `MultiSelectField` narrows its offered chips against the live record +
492
+ `current_user`, gates behind a "select the parent first" hint while a
493
+ `dependsOn` field is empty, and surfaces a legible empty state instead of a
494
+ bare chip row.
495
+ - Cascade-clear: when the offered set changes (parent changed / predicate
496
+ flipped) the widget prunes only the now-invalid selections, keeping the
497
+ still-offered ones — the array analogue of the single select's clear.
498
+ - Tests: `MultiSelectField.cascade.test.tsx` mirrors `SelectField.cascade.test.tsx`
499
+ (gating, per-element cascade clear, role/context gating).
500
+
501
+ - 1629313: feat(fields): RadioField per-option `visibleWhen` cascading + `dependsOn` gating; single-source the option resolver
502
+
503
+ Brings `RadioField` to parity with `SelectField` / `MultiSelectField` for ADR-0058
504
+ cascading & role-gated options, and collapses the three copies of the
505
+ gate-then-filter logic onto one shared resolver.
506
+
507
+ - **`@object-ui/core`**: new pure `resolveCascadingOptions(rawOptions, record, dependsOn, scope)`
508
+ → `{ options, gated, dependsOnFields }` — the single source of truth for
509
+ `dependsOn` gating + per-option `visibleWhen` filtering.
510
+ - **`@object-ui/fields`**: `RadioField` now narrows its offered radios against
511
+ the live record + `current_user`, gates behind a "select the parent first"
512
+ hint while a `dependsOn` field is empty, and clears a value no longer offered
513
+ (scalar cascade clear). The `useCascadingOptions` hook is refactored to a thin
514
+ React wrapper over `resolveCascadingOptions`.
515
+ - **`@object-ui/components`**: the form renderer's inline option pre-filter and
516
+ cross-field cascade-clear effect now call `resolveCascadingOptions` instead of
517
+ re-deriving gating/filtering, so they can't drift from the widgets (no
518
+ behavior change).
519
+ - Tests: `RadioField.cascade.test.tsx` mirrors the select cascade tests; core
520
+ gains `resolveCascadingOptions` unit coverage.
521
+
522
+ - eee4ded: feat(fields): render `select` + `multiple` through the multi-value chip picker; restore fields/core lint gates
523
+
524
+ - **Multi-value select** — a `select` field/param declared `multiple: true`
525
+ now renders the multi-value chip picker (the `multiselect` widget) and stores
526
+ a `string[]`, instead of collapsing to a single-value dropdown that could
527
+ hold only one value. The delegation lives inside `SelectField`, so the object
528
+ form, the inline grid editor, and the app-shell `ActionParamDialog` all
529
+ inherit it from the one `select` widget with no per-surface drift. Single
530
+ selects keep the cascading dropdown (multi + per-option `visibleWhen`
531
+ cascading is not a combination in use today).
532
+ - **`autonumber` mapping is unchanged** here; this change is orthogonal.
533
+ - **Lint gates restored** — fixed the pre-existing baseline lint errors that
534
+ had left the `@object-ui/fields` and `@object-ui/core` package lints red (so
535
+ the gate could not catch new violations): `react-hooks/rules-of-hooks` in
536
+ `ImageField` / `TextAreaField` / `index.tsx` (hooks hoisted above early
537
+ returns; the `useFieldTranslate` hook no longer wrapped in try/catch), plus
538
+ `no-useless-assignment` / `no-useless-escape` / `no-control-regex` /
539
+ `prefer-const` / `preserve-caught-error` in the core evaluator and utils. No
540
+ behavior change from the lint fixes.
541
+
542
+ ### Patch Changes
543
+
544
+ - 0318118: fix(app-shell): block ActionParamDialog submit while a file/image param is uploading; map spec `autonumber` (ADR-0059 follow-ups)
545
+
546
+ Two follow-ups to the shared-field-widget param rendering (ADR-0059):
547
+
548
+ - **Upload-in-progress guard.** A `file`/`image` param's value only becomes its
549
+ fileId once the presigned upload settles, so confirming mid-upload sent an
550
+ empty/stale value. `FileField`/`ImageField` now surface their upload state via
551
+ an optional `onUploadingChange` prop (shared `useUploadingSignal` hook,
552
+ ignored by other widgets); `ActionParamDialog` wires it for `file`/`image`
553
+ params and disables Confirm (label → "Uploading…", new `actionDialog.uploading`
554
+ i18n key across all locales) plus blocks submit while any upload is in flight.
555
+ - **`autonumber` spelling.** `mapFieldTypeToFormType` now maps the spec
556
+ `FieldType` spelling `autonumber` (in addition to the widget-map key
557
+ `auto_number`) to the AutoNumber widget, so a spec-typed `autonumber`
558
+ field/param no longer falls through to the plain text input — fixes the object
559
+ form path as well as action params.
560
+
561
+ - aefcf39: feat(action-params): serialize file/image action params to storage id(s); retire the approvals composer
562
+
563
+ Declared action params of `type: 'file'`/`'image'` now POST the portable API
564
+ contract — the storage id(s) — instead of the upload widget's rich object:
565
+
566
+ - `FileField` surfaces the id it already receives from the upload adapter
567
+ (`meta.fileId`) as `file_id` on each emitted file object (additive; the
568
+ record file-field value shape is unchanged).
569
+ - `ActionParamDialog` maps upload-param values to their `file_id`(s) at submit
570
+ (`serializeParamValues`, pure + exported): single → string, `multiple` →
571
+ `string[]`. The api handler already forwards param values untouched, so an
572
+ action with a `file` param POSTs `attachments: string[]`.
573
+
574
+ This lets the approvals inbox retire its last hand-wired UI — the approve/reject
575
+ composer with its bespoke attachment upload — so the drawer renders every
576
+ decision through `DeclaredActionsBar` with the declared `attachments` file param
577
+ (framework side declares it; see the paired framework change). `DeclaredActionsBar`'s
578
+ `exclude` prop stays as a general capability.
579
+
580
+ - 3b2e4d9: fix(list): route remaining system-field groupings through the shared classifier
581
+
582
+ Follow-up to the `owner_id` default-column fix: consolidate the display-oriented
583
+ system-field exclusions onto the shared `isSystemManagedField` /
584
+ `SYSTEM_MANAGED_FIELD_NAMES` (from `@object-ui/types`) so the framework-injected
585
+ `owner_id` is treated consistently across the grid, record picker, and detail
586
+ drawer.
587
+
588
+ - `ObjectGrid` record-detail drawer: the business-fields vs. muted meta-section
589
+ split now uses the shared classifier, so `owner_id` (and other injected system
590
+ fields) land in the meta section instead of the business body.
591
+ - `deriveLookupColumns` (record picker): drops its local name set for the shared
592
+ classifier — now flag-aware (`field.system`), not just name-based.
593
+ - `RecordDetailDrawer`: its default `systemFields` set is derived from the shared
594
+ `SYSTEM_MANAGED_FIELD_NAMES`; the `systemFields` prop override is preserved.
595
+
596
+ `deriveRelatedLists`' narrow "audit FK on every object" set and plugin-detail's
597
+ inline-edit "never editable" set are intentionally left distinct — different
598
+ semantics (the latter deliberately keeps `owner_id` editable).
599
+
600
+ - Updated dependencies [0318118]
601
+ - Updated dependencies [1c8935a]
602
+ - Updated dependencies [af1b0db]
603
+ - Updated dependencies [8b8b744]
604
+ - Updated dependencies [7cf4051]
605
+ - Updated dependencies [803558e]
606
+ - Updated dependencies [2e7d7f0]
607
+ - Updated dependencies [ef14f69]
608
+ - Updated dependencies [94d4876]
609
+ - Updated dependencies [1100a8b]
610
+ - Updated dependencies [7abe4cd]
611
+ - Updated dependencies [69fa5d1]
612
+ - Updated dependencies [549c67d]
613
+ - Updated dependencies [ebe6494]
614
+ - Updated dependencies [2b17339]
615
+ - Updated dependencies [31b77d4]
616
+ - Updated dependencies [6d4fbe6]
617
+ - Updated dependencies [0a3710b]
618
+ - Updated dependencies [f80aaf2]
619
+ - Updated dependencies [62b9ab5]
620
+ - Updated dependencies [1629313]
621
+ - Updated dependencies [29c6040]
622
+ - Updated dependencies [faebac3]
623
+ - Updated dependencies [2331ac9]
624
+ - Updated dependencies [199fa83]
625
+ - Updated dependencies [eee4ded]
626
+ - @object-ui/i18n@16.1.0
627
+ - @object-ui/core@16.1.0
628
+ - @object-ui/types@16.1.0
629
+ - @object-ui/react@16.1.0
630
+ - @object-ui/components@16.1.0
631
+ - @object-ui/providers@16.1.0
632
+
3
633
  ## 16.0.0
4
634
 
5
635
  ### Patch Changes