@object-ui/plugin-detail 16.1.0 → 17.2.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,819 @@
1
1
  # @object-ui/plugin-detail
2
2
 
3
+ ## 17.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a889e31: A record's approval band now shows the quorum / per-group tally the server already computes.
8
+
9
+ The showcase's `showcase_committee_quorum` node declares `behavior: 'quorum'` with
10
+ `minApprovals: 2` over three approvers, and even ships a pre-rendered
11
+ `"Committee Sign-off (2 of 3)"` label; `showcase_expense_signoff` declares
12
+ `per_group` (会签) with named manager / finance groups. On the business record
13
+ the approval band rendered none of it — the lock badge, the recall button and
14
+ the approve/reject actions were all correct, but a two-of-three committee step
15
+ looked exactly like a one-approver step. An approver could not see whether their
16
+ own click finalized the node or was one of three, which is the single fact a
17
+ quorum node exists to express (objectstack#4478).
18
+
19
+ Nothing was wrong on the wire, and nothing here papers over the server. The
20
+ framework computes `decision_progress` — `{ behavior, got, need, groups? }`,
21
+ derived from the node's own `node_config_json` snapshot, so the count a client
22
+ shows is the count the engine will enforce. **It attaches that block in
23
+ `getRequest` only**: `listRequests` deliberately skips it, because the
24
+ `sys_approval_action` tally it costs is per row and a list read may return
25
+ hundreds. The record header's `useRecordApprovals` reads
26
+ `GET /approvals/requests?object=…&recordId=…` — the list route — so the
27
+ enrichment was never in the payload it had. The hook now follows up with one
28
+ single read for the ONE pending row and folds the result onto it; a failed or
29
+ mismatched follow-up leaves the row exactly as the list sent it, so a display-only
30
+ enrichment can never take the approval panel down and no tally is ever invented.
31
+
32
+ `InlineEditProvider` carries the block through as `approvalProgress`, and the
33
+ DetailView approval band renders it beside the existing badge: a labelled
34
+ `role="progressbar"` with one tick per required approval for `quorum` /
35
+ `unanimous`, and for `per_group` a chip per group marking which have signed
36
+ (`finance 1/1` ✓, `manager 0/1`). Group names come from the flow author's own
37
+ config, so they need no locale strings; the three new label keys are added to all
38
+ ten packs. `first_response` nodes carry no `decision_progress` and are unchanged —
39
+ one decision is the whole step there, and a "1 of 1" bar would be noise.
40
+
41
+ Scored `minor` rather than `patch`: this is new observable rendering plus a new
42
+ public `approvalProgress` prop / `ApprovalProgress` type on `@object-ui/react`,
43
+ not a behavior correction inside an existing surface.
44
+
45
+ - 4a51e77: Stop declaring 14 symbols across ten packages under names `@objectstack/spec`
46
+ owns (objectui#3161, objectstack#4115 batch 7 — the long tail, one or two
47
+ entries per package). All ten packages leave the ledger, which drops from 17
48
+ collisions across 11 packages to 3 across 1.
49
+
50
+ **Renamed exports** — in every case the spec exports the same name for a
51
+ _different_ thing, so the old name was a mis-description rather than a dialect:
52
+
53
+ | package | was | now | what the spec's same-named export is |
54
+ | :------------------------- | :--------------------------------- | :--------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------- |
55
+ | `@object-ui/fields` | `FieldWidgetProps` | `FieldWidgetComponentProps` | the DECLARED field-widget plugin props contract (a zod object; `field.type` is the `FieldType` enum, `readonly`/`required` carry defaults) |
56
+ | `@object-ui/layout` | `PageHeaderProps` | `PageHeaderComponentProps` | the authored `page:header` node — a zod schema of `title`, `subtitle`, an icon NAME, `breadcrumb`, `actions: string[]` |
57
+ | `@object-ui/layout` | `Page` | `PageNodeRenderer` | the authored page metadata DOCUMENT (`name`, `label`, `type`, `regions`) |
58
+ | `@object-ui/plugin-detail` | `ObjectFieldLike` | `ObjectDefFieldLike` | the i18n duck type `translateObject` walks (`help`/`description`, plus `[key: string]: any`) |
59
+ | `@object-ui/plugin-grid` | `ColumnSummaryConfig` | `ColumnSummarySetting` | the OBJECT form of `ListColumn.summary` **only** — the local one was the whole union, shorthand included |
60
+ | `@object-ui/plugin-grid` | `isMultiValueField` | `hasMultiValueShape` | the spec's classifier, which requires a def with a `type`; the local one is called with `undefined` |
61
+ | `@object-ui/collaboration` | `RealtimeConfig` | `RealtimeSubscriptionConfig` | the app's realtime DECLARATION (`enabled`, `transport`, `subscriptions[]`) |
62
+ | `@object-ui/plugin-charts` | `ChartConfig` | `ChartContainerConfig` | the authored chart document (`type`, `xAxis`, `series`, `showLegend`, …) |
63
+ | `@object-ui/plugin-form` | `FormSection` / `FormSectionProps` | `FormSectionContainer` / `FormSectionContainerProps` | the authored form-section metadata (`name`, `pane`, `visibleWhen`, `fields`) |
64
+ | `@object-ui/providers` | `Theme` | `ThemePreference` | a whole theme DOCUMENT (`name`, `label`, `colors`, `typography`) |
65
+ | `@object-ui/runner` | `App` (default export) | `RunnerApp` | the authored application metadata type **and** the `App.create()` builder |
66
+ | `@object-ui/sdui-parser` | `ValidationResult` | `ManifestValidationResult` | plugin-manifest validation (`{ valid, errors?, warnings? }`), exported from both `kernel` and `contracts` |
67
+
68
+ `ManifestValidationResult` follows the `<what was validated>Validation<Error|Result>`
69
+ convention registered on objectstack#4115 (`@object-ui/core` took
70
+ `SchemaNodeValidationResult` in batch 4). `PageHeaderComponentProps` deliberately
71
+ reuses the name `@object-ui/app-shell` already chose for its own header props in
72
+ batch 3, so one concept does not acquire two dialect names one package apart.
73
+
74
+ **Now derived from the spec instead of hand-written:**
75
+
76
+ - `@object-ui/fields` — `isFileIdToken` is re-exported from
77
+ `@objectstack/spec/data`. The local copy was character-for-character identical
78
+ to the spec's function while its comment said it "mirrors" it, so every
79
+ behaviour test passed and only reference identity could tell the two apart.
80
+ The regex is a wire decision: widening it server-side while a copy here kept
81
+ the old bound would make every new id read as "not a reference", and the
82
+ widget would submit the legacy inline blob to a backend expecting a reference.
83
+ - `@object-ui/plugin-detail` — `FeedFilterMode` is re-exported from
84
+ `@objectstack/spec/data`, in a file that already imported the sibling
85
+ `FeedItemType` from the spec.
86
+ - `@object-ui/plugin-grid` — the eleven-member aggregation union is now the
87
+ spec's `ColumnSummary` enum, so the total `Record<ColumnSummaryType, string>`
88
+ label map turns a member the spec adds into a compile error instead of a
89
+ blank footer cell. `ColumnSummarySetting` is `NonNullable<ListColumn['summary']>`,
90
+ i.e. whatever forms the spec itself accepts. `hasMultiValueShape` delegates to
91
+ the spec's `isMultiValueField` rather than re-deriving it from
92
+ `MULTI_OPTION_TYPES` / `MULTI_CAPABLE_TYPES`.
93
+ - `@object-ui/providers` — `ThemePreference` is the spec's `ThemeMode` union
94
+ plus the one legacy `'system'` spelling this provider still honours for stored
95
+ preferences, read off the schema's own `_zod` carrier so the package takes no
96
+ zod dependency.
97
+
98
+ `@objectstack/spec` moves from `devDependencies` to `dependencies` in
99
+ `@object-ui/fields` (it re-exports a runtime function) and `@object-ui/providers`
100
+ (its public `.d.ts` now references the spec).
101
+
102
+ Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
103
+ tracks `@objectstack`, so breaking changes of our own ship as minor with the
104
+ semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
105
+ all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.
106
+
107
+ - dd06bcd: `record:activity` fetches a feed instead of rendering a permanently empty one (objectui#3165).
108
+
109
+ The block published eleven inputs — `types`, `filterMode`, `showFilterToggle`,
110
+ `limit`, `showCompleted`, `unifiedTimeline`, `showCommentInput`,
111
+ `enableMentions`, `enableReactions`, `enableThreading`,
112
+ `showSubscriptionToggle` — every one of them a filter or an affordance over a
113
+ feed that could not have content on any path. `RecordActivityRenderer` called
114
+ `useRecordContext()`, discarded the result and rendered
115
+ `<RecordActivityTimeline items={[]}>` with the empty array hard-coded; the
116
+ timeline takes `items` as a prop and never fetched; no host supplied any
117
+ (`buildDefaultPageSchema` emits `{ type: 'record:activity' }` with no props at
118
+ all). Declared, published to `sdui.manifest.json`, inert at runtime —
119
+ objectstack#4413's shape, three blocks over.
120
+
121
+ **The feed now has three sources, in precedence order.** `items` on the node
122
+ (the convention `record:history` uses for `entries`); a mounted
123
+ `DiscussionContext`, which the console's record page fills with the merged
124
+ `sys_comment` + `sys_activity` feed and the write handlers; otherwise a
125
+ **self-fetch** of `sys_activity` scoped to the bound record
126
+ (`{ object_name, record_id }`, newest first, `limit` rows per page, "Load more"
127
+ re-reading a wider window). The third path is what makes the block
128
+ drop-anywhere — hand-authored inside a `page:tabs`, with no host feeding it —
129
+ and it mirrors the read `record:history` already had. Rows map to feed items
130
+ exactly as the console's record page maps them, so both surfaces agree about
131
+ what a row is.
132
+
133
+ **The read-side inputs now filter.** `types` is an allow-list over feed item
134
+ types (unrecognised entries ignored; an all-typo list is treated as unset
135
+ rather than emptying the feed). `limit` is a page size and caps the scoped
136
+ read. `showCompleted` (spec default `false`) hides completed activities.
137
+ `unifiedTimeline: false` un-mixes field changes from the comment stream — the
138
+ panel becomes a discussion feed and field changes stay in `record:history`.
139
+ `filterMode` seeds which slice the dropdown opens on and falls back to `all`
140
+ on an unrecognised value instead of leaving a `<Select>` matching nothing.
141
+
142
+ **The write-side switches are wired to the host's handlers.**
143
+ `showCommentInput`, `enableThreading`, `enableReactions` and `enableMentions`
144
+ read `onAddComment` / `onAddReply` / `onToggleReaction` /
145
+ `mentionSuggestions` off `DiscussionContext` — the same standing
146
+ `record:discussion` has. With no host mounted the feed stays read-only and no
147
+ composer is rendered, rather than showing one that silently drops what you
148
+ type.
149
+
150
+ **`showSubscriptionToggle` is recorded as a known gap, not quietly left
151
+ looking configurable.** The bell needs a `RecordSubscription` value and
152
+ somewhere to persist it, and the platform has no record-subscription object to
153
+ read or write one from. Its input description now says `NOT IMPLEMENTED` (that
154
+ text ships to `sdui.manifest.json`, so an author meets it before writing the
155
+ prop), the docs repeat it, and a test pins it inert so the note has to be
156
+ deleted the day a backend for it lands.
157
+
158
+ `apps/console`'s record-reach probe (objectui#3149 layer 3a) asserted the old
159
+ behaviour from its `NO_RECORD_REACH` ledger in both directions; that entry is
160
+ deleted, and the probe now reports `record:activity` as responding to the bound
161
+ record.
162
+
163
+ ### Patch Changes
164
+
165
+ - 6be575c: A fetching activity feed says "loading", not "No activity recorded" (objectui#3205).
166
+
167
+ `RecordActivityTimeline` declared a `loading` prop and destructured it straight
168
+ into `_loading`, so nothing in the component ever read it. For the whole
169
+ duration of a feed fetch the panel therefore rendered the empty state — "No
170
+ activity recorded" — and then contradicted itself when the rows landed. The
171
+ empty copy is a factual claim about the record; it may only be made once the
172
+ fetch that would fill the feed has settled.
173
+
174
+ The timeline now branches on `loading` **before** the empty branch and renders a
175
+ spinner row (`role="status"`, `aria-live="polite"`) while the feed is in flight.
176
+ `collapseWhenEmpty` does not suppress it: that flag is about the empty state
177
+ ("collapse when there are no items"), and mid-fetch it is not yet known whether
178
+ there are items.
179
+
180
+ The guard is `loading && filtered.length === 0`, not `loading` alone — a refresh
181
+ or a "Load more" round-trip keeps the rows already on screen instead of blanking
182
+ them (that button carries its own spinner).
183
+
184
+ No prop or signature changed: the fix is that the declared prop is now read.
185
+ `record:activity` has computed a live `loading` since objectui#3165 (true during
186
+ its self-fetch) and `RecordChatterPanel` already forwarded it in both positions,
187
+ so the whole chain now shows a loading state end to end.
188
+
189
+ - Updated dependencies [a889e31]
190
+ - Updated dependencies [4b470b9]
191
+ - @object-ui/i18n@17.2.0
192
+
193
+ ## 17.1.0
194
+
195
+ ### Minor Changes
196
+
197
+ - 1cf0de7: fix(detail): finish the approval-lock story, and warn on silently stripped fields (framework#3794)
198
+
199
+ The Console reported record writability wrong in both directions during an
200
+ approval, so a user had nothing to go on: what they _could_ edit said "locked",
201
+ and what they _couldn't_ said "updated successfully".
202
+
203
+ **The lock band told the truth; the Edit button did not.** objectui#2902 split
204
+ the band into "in approval · editable" vs locked, but the header **Edit** CTA
205
+ still keyed off nothing at all — on a genuinely locked record it stayed live, so
206
+ the user opened the form, filled a screen, and got `RECORD_LOCKED` back on Save.
207
+ It is now `disabled` on a locked record: visible-but-off, with the band beside it
208
+ saying why. This is the LOCK, not the mere presence of an approval — a
209
+ `lockRecord: false` node keeps Edit live, which is the point of that setting.
210
+
211
+ **And the band could still re-lock itself.** `DetailView` OR-ed the record's own
212
+ `approval_status` mirror into `isLocked` unconditionally. That mirror is written
213
+ on submit by any flow configuring an `approvalStatusField`, _regardless of_
214
+ `lockRecord` — so on a `lockRecord: false` node the host correctly resolved "not
215
+ locked" from the request's `lock_record` while the mirror dragged the band back
216
+ to "Locked for approval", with the pencils live and saves landing underneath it.
217
+ The host is now authoritative whenever it threads `approvalPending`; the mirror
218
+ is consulted only for bare/legacy `DetailView` hosts that thread nothing, where
219
+ it still reads as locked (no node granularity — the safe direction).
220
+
221
+ Recall's tooltip no longer promises to unlock a record the node never locked
222
+ (`detail.cancelApprovalTooltipUnlocked`).
223
+
224
+ **Silently stripped fields now surface on the record form's save path.** The
225
+ adapter emitted a write-warning for `create`/`update` responses carrying
226
+ `droppedFields`, but not for `batchTransaction` — which is how the record form
227
+ saves a master-detail record, i.e. the one surface where a user actually edits a
228
+ `readonlyWhen`-locked field. `batchTransaction` now emits one warning per event,
229
+ resolving each back to its operation via the response's `index`.
230
+
231
+ The toast itself was hardcoded English and called every strip "read-only". It is
232
+ now localized (`detail.writeStripped*`, ten locales) and worded by reason:
233
+ `readonly_when` says the field is not editable _in this record's current state_,
234
+ which is what actually happened — the field is editable in other states and the
235
+ form rendered it as an ordinary input, so "read-only" sent the user hunting for a
236
+ permission problem that does not exist.
237
+
238
+ **And it stopped crying wolf.** `createObjectStackUserStateAdapter` hand-stamped
239
+ the server-managed `updated_at` on every recents/favorites write, which the
240
+ server strips and reports — so the console popped "Some fields were not saved"
241
+ about a field no user ever touched, on page loads, drowning the signal the toast
242
+ exists for. It no longer sends the column; the server stamps it anyway.
243
+
244
+ - 390c071: feat(record): declare inputs for the seven configurable record:\* blocks, and curate six
245
+
246
+ Seven `record:*` blocks shipped with renderers that read props but declared no
247
+ `inputs`. That combination is the worst of both: the renderer honours
248
+ `limit`, `severity`, `location` …, while every authoring surface — the designer
249
+ panel, the AI vocabulary, the generated manifest — reports the block takes no
250
+ configuration. objectui#3013 recorded them as deliberately uncurated for
251
+ exactly that reason.
252
+
253
+ The declarations mirror what each renderer actually reads:
254
+
255
+ | block | inputs |
256
+ | -------------------------------------- | ---------------------------------------------------------------------------- |
257
+ | `record:activity` | 11 — from `RecordActivityComponentProps` |
258
+ | `record:chatter` / `record:discussion` | 5 — from `RecordChatterComponentProps` |
259
+ | `record:alert` | 8 — severity, title, body, visible, icon, action, dismissible, dismissKey |
260
+ | `record:quick_actions` | 7 — actionNames, requiredPermissions, location, align, inline, variant, size |
261
+ | `record:history` | 3 — limit, emptyText, unknownUserText |
262
+ | `record:reference_rail` | 1 — hideEmpty |
263
+
264
+ `inputs` describe what an AUTHOR writes, which is a subset of what the renderer
265
+ reads. `entries`, `loading` and resolved `actions` are injected by the host
266
+ shell off RecordContext; declaring them would invite a model to hand-write the
267
+ data the page is supposed to fetch. `aria` is omitted for the reason it is
268
+ omitted on `record:details` — an accessibility escape hatch, not a layout
269
+ choice. `location` takes its enum from the spec's `ACTION_LOCATIONS` rather
270
+ than restating it, per objectui#3019.
271
+
272
+ Six of the seven are now in `PUBLIC_BLOCKS`: configurable and absent from the
273
+ contract is the state objectui#3006 was about. The contract goes 36 → 42 tags,
274
+ all resolving.
275
+
276
+ `record:chatter` stays out — it is the same renderer as `record:discussion`
277
+ under a Salesforce-familiar name, kept for schemas already in the wild. Two
278
+ spellings of one block is ambiguity an authoring model cannot resolve, so the
279
+ vocabulary carries the spec's name. A test compares the two input lists, so the
280
+ day they diverge the exclusion stops being justified and fails.
281
+
282
+ A companion assertion requires every curated `record:*` tag to declare inputs.
283
+ A curated tag with none reads as "takes no configuration" when the renderer in
284
+ fact reads props — the same gap objectui#3006 opened, pointed the other way.
285
+
286
+ - bac266c: fix(detail): a related list has one sorting semantics instead of two — #3106
287
+
288
+ A related list carried two. Its own sort-button row (opt-in via `sortable`) went
289
+ out as a server `$orderby` over the whole child collection; the `data-table` it
290
+ embeds took `sortable`'s default of `true` and sorted the rows it was holding —
291
+ which, in windowed mode, is **one page**.
292
+
293
+ Turning `sortable` on put both in the same card, with nothing saying they meant
294
+ different things. Leaving it off — the default — was worse: the page-local sort
295
+ was then the _only_ one the user could reach, and it looked exactly like the
296
+ list being sorted.
297
+
298
+ The table's column headers now drive this list's sort in both modes, so there is
299
+ one order behind them:
300
+
301
+ - **Windowed**: the header sort becomes the server `$orderby` and resets to page
302
+ one, the same path the buttons took.
303
+ - **Client mode**: this list keeps sorting in memory, where its key is the label
304
+ resolved through its own id → name map (#3096) — a key the embedded table
305
+ cannot see, so its sort was the worse of the two even when both were possible.
306
+
307
+ The button row survives only where there are no headers to click: a `list`
308
+ (`data-list`) related list, or a caller-supplied `schema` whose contents we
309
+ cannot assume. `sortable`'s documentation now says that is what it controls.
310
+
311
+ Relational columns keep #3096's rule, moved to the header: no sort affordance
312
+ while the sort is a server `$orderby` (the key would be the stored foreign-key
313
+ id while the cell shows a name), live in client mode where the key is the label.
314
+
315
+ ### Patch Changes
316
+
317
+ - fc0272a: fix(actions): apply the ADR-0066 D4 capability gate on every action surface (framework#3923)
318
+
319
+ An action declaring `requiredPermissions` is supposed to be one declaration with
320
+ two enforcement surfaces: 403 on the server, hidden button in the UI. The UI half
321
+ only ever ran inside `ActionEngine.getActionsForLocation` — and the surfaces
322
+ `record_header`, `record_more`, `list_item` and `list_toolbar` actually render on
323
+ do not go through the engine. They filter their own action lists. So a button
324
+ declaring a capability nobody holds rendered, live and clickable, on the record
325
+ header, in every grid row menu, and on the list toolbar. For a `type: 'api'`
326
+ action pointed at a self-authored endpoint, nothing else was checking either: the
327
+ platform's action route (which is where the 403 comes from) never sees that
328
+ request.
329
+
330
+ `page:header`, `action:bar` (business _and_ `systemActions`) and the grid's
331
+ `RowActionMenu` now apply the same gate, via a shared `useCapabilityGate()` so
332
+ the surfaces cannot drift apart. The rule is the engine's, unchanged: hide unless
333
+ the caller holds **all** declared capabilities; an empty held set is "holds
334
+ nothing" and gates; **unknown** — no action runtime, no resolved capabilities —
335
+ fails OPEN, because the server is the authority and hiding a permitted user's
336
+ button on missing client data is the worse failure.
337
+
338
+ The record surface was also feeding the gate nothing to work with.
339
+ `RecordDetailView` mounts its own `<ActionProvider>`, which shadows the shell's
340
+ for every action on that page, and seeded it with identity only — no
341
+ `systemPermissions`. Since unknown fails open, that alone un-gated every
342
+ `record_header` / `record_more` / `record_section` action on the one page those
343
+ locations exist on. It now forwards the caller's resolved capabilities (and only
344
+ once they have actually resolved, so a standalone embed without a
345
+ `PermissionProvider` keeps failing open rather than hiding everything).
346
+
347
+ `useRecordEditable`'s record-level explain probe went out on a bare
348
+ `fetch(..., { credentials: 'include' })`. A bearer-token session carries its
349
+ credential in the `Authorization` header, not a cookie, so the probe came back
350
+ 401 on a perfectly valid admin session and the verdict silently failed open —
351
+ the hook was inert in exactly the deployments it was written for. It now rides
352
+ the host's authenticated fetch (`SchemaRendererProvider`'s `apiFetch`), falling
353
+ back to the global one for standalone embeds.
354
+
355
+ - c785740: fix(detail): record Attachments become their own tab (with count badge) and their copy is translated — objectstack#4358
356
+
357
+ Two defects on `enable.files: true` record detail pages:
358
+
359
+ 1. **Buried placement.** `RecordDetailView` appended `RecordAttachmentsPanel`
360
+ AFTER the schema-rendered page tree, whose synthesized default embeds
361
+ `record:discussion` as the last main component — so the panel always
362
+ landed below an ever-growing feed timeline, undiscoverable without
363
+ scrolling to the very bottom, with no metadata knob to move it.
364
+
365
+ `buildDefaultTabs` now emits a peer **Attachments** tab (a new
366
+ `record:attachments` node rendered by an app-shell registration wrapping
367
+ the existing panel via RecordContext) between Related and
368
+ Activity/History. `PageTabsRenderer` derives the tab's count badge from a
369
+ `sys_attachment` probe scoped to `(parent_object, parent_id)`, riding the
370
+ same RelatedCountStore cache/invalidation bus as related-list badges — so
371
+ uploads and deletes update the badge live. A `hideAttachments` synthesizer
372
+ option suppresses the tab; RecordDetailView keeps its legacy bottom append
373
+ only as the fallback for authored pages without the node
374
+ (`hasExplicitAttachments`).
375
+
376
+ 2. **Untranslated copy.** The panel's eleven `detail.*` keys (`attachments`,
377
+ `uploadAttachment`, `loadingAttachments`, `noAttachments`,
378
+ `downloadAttachment`, `deleteAttachment`, and the five
379
+ `attachment*Denied/Required` friendly errors) existed only as inline
380
+ English `defaultValue`s — no locale bundle carried them, so non-English
381
+ consoles always showed English. All ten locales now define them; the tab
382
+ label rides the existing well-known-label dictionary (→ 附件 etc.).
383
+
384
+ - 95b7214: fix(list,grid,detail,tree,core): every column resolver reads one key (#3104 PR2)
385
+
386
+ PR1 (#3119) put a canonicalizing fold at ListView's ingestion boundary. This
387
+ converges the 22 read sites themselves onto `columnIdentity()` from
388
+ `@object-ui/core`, so a surface that is NOT downstream of that fold resolves
389
+ the same identity anyway.
390
+
391
+ That distinction is the user-visible part. A standalone `object-grid` node —
392
+ authored directly on a page, with no `list-view` above it — never passed
393
+ through `normalizeListViewSchema`. Its `getSelectFields` read `c.field` alone
394
+ while the `ensureId` probe one line above read `f?.name || f?.field`, so a
395
+ legacy `{ name: 'account' }` column reached `$select` as a literal `undefined`
396
+ hole: the server never returned the field and every cell in that column came
397
+ back empty. Same for `ObjectTree`, `RelatedList` and the `record:details` /
398
+ `record:related_list` renderers.
399
+
400
+ Converged:
401
+
402
+ | Surface | Was | Now |
403
+ | ---------------------------------------- | ---------------------------------------------- | ----------------------------------- |
404
+ | `ListView` ×9 + its 2 request builders | `name \|\| fieldName \|\| field` vs `f?.field` | `columnIdentity()` |
405
+ | `RelatedList` ×8 | `accessorKey \|\| field \|\| name` | `accessorKey \|\| columnIdentity()` |
406
+ | `ObjectGrid` | name-first probe vs `c.field` projection | `columnIdentity()` |
407
+ | `ObjectTree` | `name \|\| fieldName \|\| field \|\| key` | `columnIdentity() \|\| key` |
408
+ | `buildExpandFields` | `field ?? name ?? fieldName` | `columnIdentity()` |
409
+ | `record-details` / `record-related-list` | `field \|\| name (\|\| key)` | `columnIdentity() (\|\| key)` |
410
+
411
+ `accessorKey` keeps its precedence in `RelatedList` — it is TanStack Table's
412
+ column key, not ObjectStack metadata identity, and only the `field || name`
413
+ tail was converged. `key` stays a tail fallback in `ObjectTree` and
414
+ `record-related-list` for the same reason: it is a generic entry key.
415
+
416
+ Two incidental fixes that TypeScript surfaced once the resolver stopped
417
+ returning `any`: ListView's filter-field options and its hide-fields popover
418
+ both built entries keyed `undefined` for a column with no resolvable identity.
419
+ Those entries could never match a column; they are now dropped.
420
+
421
+ **Inventory re-triage.** PR1 recorded 24 family members. Two were mis-classified
422
+ and are reclassified here rather than converged — reading what they actually
423
+ feed shows they are not column reads at all:
424
+
425
+ - `ViewPreview.tsx` adapts a ViewItem **form** section to what `object-form`
426
+ selects by (`field` → `name`) — the #3090 two-layer join.
427
+ - `SchemaForm.tsx` renders an arbitrary metadata **array** into a popover
428
+ summary and guesses at a display key; the entries are validations, actions,
429
+ or whatever the JSON schema declares.
430
+
431
+ So the family was 22, and it is now **0**. The ratchet asserts that, asserts
432
+ each converged surface actually routes through the shared reader (a surface
433
+ that dropped identity resolution instead of converging it goes red), and pins
434
+ `accessorKey`'s precedence in `RelatedList`.
435
+
436
+ - 2baa13f: fix(record): register the record:\* blocks under one key, prefixed once
437
+
438
+ Eleven blocks in plugin-detail were registered as
439
+ `register('record:x', …, { namespace: 'record' })` — an already-prefixed name
440
+ handed to a registry that prefixes it again. Each landed at
441
+ `record:record:x`, and the key authors actually resolved, `record:x`, was the
442
+ un-namespaced _fallback_ rather than the intended registration. The registry
443
+ carried 23 keys for 12 components.
444
+
445
+ Nothing failed, which is why it survived: `getPublicConfigs()` rewrites `type`
446
+ to the curated tag, so the doubled name never reached the contract, the
447
+ manifest, or the JSX surface. It was visible only when enumerating the registry
448
+ directly — which is what objectui#3013's reverse check does.
449
+
450
+ Registering the bare name is what makes `namespace` correct, and
451
+ `skipFallback: true` is what keeps the fallback from claiming that bare name
452
+ globally. Without it these would take over `details`, `path`, `history`,
453
+ `alert` … as top-level tags; `alert` is the live case, owned by `ui:`. Every
454
+ block stays reachable exactly as `record:<name>`, and 23 keys become 12.
455
+
456
+ `record:line_items` needed no change — it was the one already registered this
457
+ way, which is what made objectui#3006's near-miss possible in the first place.
458
+
459
+ Two console assertions hold the shape: no key carries a doubled prefix, and no
460
+ `record:*` block owns the bare spelling of its own name.
461
+
462
+ - 2d5d594: fix(list,detail): sorting a lookup column no longer orders by an invisible key — #3096
463
+
464
+ A relational column (`lookup` / `master_detail` / `user` / `tree`) never holds
465
+ the string its cell shows: it holds the `$expand`-ed record, or a raw foreign-key
466
+ id whose label was resolved separately. Every sort path took that raw value as
467
+ its key, so the column of names came back in an order with no relation to the
468
+ names — sorting looked broken, with nothing saying the key was something else.
469
+
470
+ The two halves are fixed differently, because they can order by different things:
471
+
472
+ - **Client-side sorts** (grid column headers, any `data-table`, a non-windowed
473
+ related list) now key off the label the cell renders, via the new
474
+ `getSortValue` / `compareSortValues` in `@object-ui/core` — which resolves an
475
+ expanded record through `getRecordDisplayName` (ADR-0079), so the sort key and
476
+ the lookup cell agree on which field names a record. This replaces two broken
477
+ comparators: `a[col] < b[col]` is always false between two objects (the
478
+ comparator collapsed to a constant and permuted the rows), and
479
+ `String(a[col])` is `"[object Object]"` (every row compared equal, so the sort
480
+ silently did nothing).
481
+ - **Server `$orderby` sorts** cannot be fixed here — the key is the stored id by
482
+ construction, and `objectstack#4256` settled that no relation join is coming.
483
+ So those entry points stop offering the illusion: the ListView toolbar sort
484
+ picker withholds relational fields and explains why (pointing at a formula
485
+ field as the supported way to sort by a related name), and a windowed related
486
+ list renders no sort button for them.
487
+
488
+ A relational field the view's CURRENT sort already uses stays listed, labelled
489
+ `(by ID)`, so view metadata authored or saved with such a sort round-trips
490
+ instead of rendering a blank row and losing the sort on the next edit.
491
+
492
+ - Updated dependencies [1cf0de7]
493
+ - Updated dependencies [752e18f]
494
+ - Updated dependencies [c785740]
495
+ - Updated dependencies [d61efd1]
496
+ - Updated dependencies [9eb932b]
497
+ - Updated dependencies [3cb9646]
498
+ - Updated dependencies [d21794c]
499
+ - Updated dependencies [b5b97e2]
500
+ - Updated dependencies [2d5d594]
501
+ - Updated dependencies [f8a95e5]
502
+ - @object-ui/i18n@17.1.0
503
+
504
+ ## 17.0.0
505
+
506
+ ### Minor Changes
507
+
508
+ - 952b978: fix(detail): the approval band honors the node's `lockRecord` instead of assuming every approval locks (#2902)
509
+
510
+ A record detail page treated "a pending approval request exists" as "this
511
+ record is locked". An approval node declares `lockRecord` (default `true`), and
512
+ on `lockRecord: false` the server keeps accepting writes for the whole time
513
+ that node waits — so the console was asserting a lock the backend did not
514
+ enforce.
515
+
516
+ The label was the smaller half of it. The same conflated signal fed `canEdit`,
517
+ so the record-level inline-edit session was suppressed too: no pencils,
518
+ `enter()` a no-op. On a single-approver step — a department head or plant
519
+ manager, exactly the case `lockRecord: false` exists for, where the approver is
520
+ meant to fill in the missing detail before deciding — the capability was
521
+ unreachable from the UI. And a flow chaining nodes with different policies drew
522
+ one identical band for "edit freely" and "the server will reject your save with
523
+ `RECORD_LOCKED`", so the two states were indistinguishable until Save failed.
524
+
525
+ Approval state is now two signals:
526
+
527
+ - **`approvalPending`** — an approval is running. Drives the band and the recall
528
+ button, both meaningful whether or not the record is editable.
529
+ - **`locked`** — that approval also forbids edits, from the pending node's
530
+ `lock_record` (framework#3814, read off the same `node_config_json` snapshot
531
+ the server's record-lock hook reads).
532
+
533
+ The band renders two states: amber lock + "Locked for approval", or sky clock +
534
+ "In approval · editable", each with its own tooltip. Recall moved out of the
535
+ locked branch — an editable pending approval is just as recallable. Inline
536
+ editing stays live in the editable state.
537
+
538
+ `InlineEditProvider` takes a new optional `approvalPending` prop, defaulting to
539
+ `locked`, so a host that threads only `locked` renders exactly as before. The
540
+ record's `approval_status` field remains the fallback for backends with no
541
+ approvals API; it carries no node granularity, so it still reads as locked — as
542
+ does a pending request from a backend too old to report the policy.
543
+
544
+ New `detail.approvalPendingEditable` / `detail.approvalPendingTooltip` keys are
545
+ translated in all ten locales.
546
+
547
+ - f9bbddb: feat: gate detail/form edit & delete on the server's effective operation set (#3546)
548
+
549
+ PR-4 (#3391) wired the **list/toolbar** surface (ObjectView Import, ListView /
550
+ ObjectGrid Export) to the server-resolved effective API operation set
551
+ (`/me/permissions` `apiOperations`, intersected via
552
+ `resolveCrudAffordances(obj, effectiveApiOperations?)`). The **detail / form**
553
+ surfaces still gated edit/delete on the bucket + `userActions` alone. This
554
+ extends the same intersection to them, so the record page and its forms never
555
+ offer an operation the server would 405.
556
+
557
+ - **core** `isObjectInlineEditable(obj, effectiveApiOperations?)` gains the same
558
+ optional second argument as `resolveCrudAffordances` — inline-edit is now
559
+ additionally ANDed with the server allowing `update`.
560
+ - **app-shell** `RecordDetailView` threads the object's effective operations into
561
+ the synthesized Edit/Delete header actions and the record-body inline-edit
562
+ gate (`canEdit`); `RelatedRecordActionsBridge` intersects each **child**
563
+ object's Create/Edit/Delete handlers with that child's own effective set.
564
+ - **plugin-detail** `record:details` ANDs its inline-edit affordance with the
565
+ object's effective `update`.
566
+ - **plugin-form** `ObjectForm`'s blanket managed-object field lock also engages
567
+ when the server denies `update` (edit mode) / `create` (create mode).
568
+
569
+ Backward-compatible: a missing effective set (unrestricted object, older
570
+ backend, or no `PermissionProvider`) leaves the resolved affordance untouched —
571
+ the bucket/`userActions` decision wins, exactly as today. Layers on top of the
572
+ existing per-object `check('edit')` / `check('delete')` permission gates
573
+ (intersection, never union).
574
+
575
+ ### Patch Changes
576
+
577
+ - 7b21891: fix(action): honor the spec `disabled` predicate on every action-rendering surface (#1885 follow-through)
578
+
579
+ The spec Action field is `disabled` (boolean | CEL — disabled when TRUE); the
580
+ schema has no `enabled` key. #1885 wired it in `action:button` only. Browser
581
+ dogfooding against the showcase found FIVE more surfaces where a spec-authored
582
+ `disabled` silently did nothing:
583
+
584
+ - **components** — the `action:group` leaves (inline + dropdown), `action:icon`
585
+ and `action:menu` still read the legacy non-spec `enabled`. They now consume
586
+ `disabled` as the primary control (evaluated in the same scope as `visible`),
587
+ with `enabled` kept as a deprecated fallback.
588
+ - **app-shell** — `DeclaredActionsBar` (server-declared action bar) read
589
+ neither; it gains `disabled` (no legacy fallback: declared actions are
590
+ spec-shaped and never carried `enabled`).
591
+ - **plugin-detail** — `record:quick_actions` HAD a `disabled` implementation,
592
+ but its `typeof === 'string'` split dropped the `{dialect:'cel', source}`
593
+ envelope the server compiles authored CEL into (#2661 routes envelopes to the
594
+ canonical formula engine), so the predicate never fired on real metadata. It
595
+ now feeds `toPredicateInput`'s result to `useCondition` whole, like every
596
+ other surface.
597
+
598
+ Pinned by new `DropdownActionItem` tests (disabled-when-TRUE, false-stays-
599
+ clickable, disabled-wins-over-enabled, boolean literal) and browser-verified
600
+ end-to-end against the showcase `showcase_archive_task` specimen: greyed on an
601
+ in-progress task, clickable on a done one (with `visible` hiding Mark Done on
602
+ the same screen — the hide-vs-grey contrast).
603
+
604
+ - faad45e: fix(fields): render `image` fields consistently and add click-to-zoom (#2836)
605
+
606
+ An `image` field rendered differently — and wrongly — on three surfaces:
607
+
608
+ - **Edit form showed broken thumbnails.** A record read back its `image` value
609
+ as a bare `sys_file` id (the reference form), but `readFileValue` returned an
610
+ id with no URL — the comment assumed the read path expands it, which the
611
+ edit-form data path does not. The result was `<img src="">`. `file-value` now
612
+ derives the stable download URL (`/api/v1/storage/files/:id`, which
613
+ 302-redirects to a signed URL and works directly as `<img src>`) for a bare
614
+ id or an id-only object, so every widget and cell renderer resolves one.
615
+ - **Inline edit leaked the raw storage URL.** `InlineFieldInput` had no branch
616
+ for file-backed types and fell through to a plain text input showing
617
+ `/api/v1/storage/files/…`. It now renders the same upload widgets the form
618
+ uses (`image`/`avatar`/`signature`/`file`/`video`/`audio`).
619
+ - **Hard-coded English.** `ImageField`'s upload/crop/remove/alt strings now go
620
+ through `t('fields.image.*')` (en + zh added).
621
+
622
+ Also adds an `ImageLightbox` — click a read-only thumbnail (detail or list cell)
623
+ to open a full-screen preview; multiple images get prev/next navigation, a
624
+ position counter and arrow-key support, a single image just the image. In a
625
+ grid cell the click is `stopPropagation`-guarded so enlarging doesn't also open
626
+ the row.
627
+
628
+ - 7d46648: fix(hooks): stop calling translation hooks inside try/catch (objectui#2879)
629
+
630
+ Eleven call sites wrapped a React hook in `try`/`catch` to make it
631
+ "provider-safe". `useObjectTranslation` and `useObjectLabel` already are — they
632
+ read context optionally and fall back to react-i18next's global instance, and
633
+ never throw. The `catch` bought nothing and cost correctness: a throw _after_
634
+ the hook ran desyncs hook order on the next render, because React matches hooks
635
+ positionally. objectui#2595/#2596 fixed exactly this in `@object-ui/i18n`'s
636
+ `createSafeTranslation`; nine plugin-local re-implementations kept their own
637
+ copy of the bug, and two more (`ObjectTimeline`, `ObjectView`) were found by the
638
+ new lint rule below — `ObjectView` had even suppressed
639
+ `react-hooks/rules-of-hooks` inline to keep it.
640
+
641
+ - Six exact re-implementations now delegate to `createSafeTranslation`:
642
+ `plugin-detail`, `plugin-timeline`, `plugin-list`, `plugin-calendar`,
643
+ `plugin-grid`'s `ObjectGrid`, `plugin-designer`.
644
+ - `components`' `data-table` also delegates; `createSafeTranslation` now
645
+ returns `language` alongside `t` so consumers that localize dates don't need
646
+ a second hook call. Purely additive.
647
+ - `plugin-gantt` and `plugin-grid`'s `ImportWizard` keep their local hooks —
648
+ they fall back _per key_, which a single-probe factory cannot express and
649
+ which their comments justify (a host dictionary that covers common keys but
650
+ lags on newer ones). Only the `try`/`catch` is removed.
651
+ - `ObjectTimeline` and `ObjectView` call the hook directly and probe the
652
+ returned value, mirroring `useSafeFieldLabel`.
653
+
654
+ Adds `object-ui/no-try-catch-around-hook` (error) so a twelfth copy fails CI.
655
+ It only matches `use*` names, accepts member calls solely on `React` (so
656
+ `vi.useRealTimers()` is not a hook), and resets its try-depth inside nested
657
+ functions (so `renderHook(() => useThing())` inside a `try` is fine) — both
658
+ false positives were real code in this repo and are pinned in the rule's tests.
659
+
660
+ `eslint-rules/**/*.test.js` matched no vitest project glob, so the local
661
+ plugin's specs had never run in CI. They are now included; all three pass.
662
+
663
+ `ObjectTimeline`'s test mock of `@object-ui/react` omitted `useObjectLabel` —
664
+ the removed `try`/`catch` had been silently absorbing that gap. The mock is now
665
+ complete.
666
+
667
+ - 53642d4: fix(core,fields): a string `$orderby` is a clause, not a character array — and localize the sharing-rule widgets (objectstack#3821)
668
+
669
+ **The recipient picker listed nothing, ever.** `QueryParams['$orderby']` was
670
+ typed as `Record | string[] | SortObject[]`, so `queryParamsToRecord` sent any
671
+ non-array value through `Object.entries`. Handed the clause string `'name asc'`
672
+ — which callers do build by hand — it walked the string index by index and
673
+ emitted `$orderby=0 n,1 a,2 m,3 e,4 ,5 a,6 s,7 c`. The server sorted by columns
674
+ that don't exist and every row was filtered out, so
675
+ `sys_sharing_rule.recipient_id` rendered "No matches" for every recipient type
676
+ and no sharing rule could be created from the Console. `ObjectGrid` builds the
677
+ same shape from a schema-level `sort` in three places, so grids with a string
678
+ sort silently showed an empty table.
679
+
680
+ A string `$orderby` is now passed through verbatim (the server's OData
681
+ normalizer has always parsed `'name asc'`), and the type admits `string`.
682
+ `RecipientPickerField` additionally switched to the structured
683
+ `{ name: 'asc' }` form so it can't regress this way against any data source.
684
+
685
+ **The three sharing-rule authoring widgets never had translations.**
686
+ `ObjectRefField`, `RecipientPickerField` and `FilterConditionField` hardcoded
687
+ their English copy — a Chinese Console showed "Select an object", "Select a
688
+ user", "Search…", "No matches", "Edit as JSON". They now go through
689
+ `useFieldTranslation` like every other widget, with keys added under `fields.*`
690
+ in all ten locales.
691
+
692
+ The recipient placeholder was the interesting one: it read
693
+ `` `Select a ${recipientType.replace(/_/g,' ')}` ``, interpolating the enum
694
+ value into an English sentence — a shape no locale can translate. It is now a
695
+ per-type key (`fields.recipient.selectUser`, `…selectBusinessUnit`, …), so
696
+ "选择业务单元" and "Select a business unit" no longer have to share a structure.
697
+
698
+ **Editing a rule silently dropped its recipient.** The picker resets the stored
699
+ id when `recipient_type` changes, because an id valid for a user is meaningless
700
+ for a team. It treated the edit form's `'' → 'user'` hydration as such a change:
701
+ opening any saved rule blanked the recipient, and saving persisted the blank.
702
+ Only a non-empty predecessor now counts as a type switch.
703
+
704
+ **Building a filter submitted the surrounding form.** None of `FilterBuilder`'s
705
+ controls declared `type="button"`, and a bare `<button>` inside a `<form>`
706
+ defaults to `type="submit"`. Adding, removing or clearing a condition therefore
707
+ submitted the sharing-rule dialog — firing validation mid-edit, and on an
708
+ already-valid form saving the record before the admin was done.
709
+
710
+ **A rejected write showed the user raw server diagnostics.** The form rendered
711
+ `error.message` verbatim, so a sharing / RLS denial reached the dialog and the
712
+ toast as `FORBIDDEN: insufficient privileges to update showcase_private_note
713
+ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and the
714
+ record id to whoever hit it. Permission failures now render localized copy
715
+ (`form.noPermissionToSave`, added in all ten locales), with the server text kept
716
+ on the console for debugging; other failures still show the server's message,
717
+ which is the useful part, and fall back to `form.submitFailed` when there is
718
+ none — replacing the previously hardcoded English "An error occurred during
719
+ submission".
720
+
721
+ **The detail header offered "Edit" on records the user may only read.** Object
722
+ permissions can't express "this one record is read-only" — a read-only sharing
723
+ grant sits inside an object the user may otherwise edit — so the header showed
724
+ the primary Edit CTA, opened the form, and let the user retype a field before
725
+ the server rejected the save. `DetailView` now gates Edit / Delete on the
726
+ object-level check AND on the explain engine's record-grained verdict
727
+ (`POST /api/v1/security/explain` with a `recordId`, ADR-0090 D6 / ADR-0095 C2 —
728
+ the same pipeline the enforcement middleware runs, so button and server cannot
729
+ disagree). Explaining oneself needs no special permission. The probe is one
730
+ cached request per record, skipped entirely when the object-level check already
731
+ says no, and **fails open** on every uncertainty — an unanswered hint must never
732
+ be the reason a permitted user cannot act; the server stays the authority
733
+ (ADR-0057 D10).
734
+
735
+ **A long option rendered straight past the combobox border.** `Combobox`'s
736
+ trigger pinned itself to the component's `w-[200px]` default while the fields
737
+ around it ran the full form column, and the selected label was a bare text child
738
+ of a flex button — flex items need `truncate` AND `min-w-0` to clip, and it had
739
+ neither. So "成员 (showcase_project_membership)" in the object picker overflowed
740
+ the control and collided with the field beside it. The label now truncates, the
741
+ trigger can shrink, the dropdown matches the trigger's width instead of a
742
+ hardcoded 200px (a widened combobox used to clip its own options), and the two
743
+ sharing-rule pickers ask for `w-full` so they line up with every other input.
744
+
745
+ Hardens `evaluatePermission` while there: a role config carrying only
746
+ `fieldPermissions` (no `actions`) made `check()` throw a TypeError that
747
+ propagated out of the render. A permission check must not be able to crash a
748
+ view.
749
+
750
+ Browser-verified against the framework showcase Console in Chinese: object /
751
+ criteria / recipient copy is fully localized, the recipient dropdown lists real
752
+ users, business units and positions, a saved rule reopens with its recipient and
753
+ criteria intact, editing the filter no longer submits, and a rule created
754
+ end-to-end stores a real record id rather than free text. The criteria authored
755
+ in the builder is honored by the evaluator: `{"pinned":true}` on an owner-private
756
+ object granted the recipient exactly the matching records and nothing else.
757
+
758
+ - Updated dependencies [7b21891]
759
+ - Updated dependencies [0b3be01]
760
+ - Updated dependencies [3c4d935]
761
+ - Updated dependencies [4b1ed7d]
762
+ - Updated dependencies [4b60d2d]
763
+ - Updated dependencies [952b978]
764
+ - Updated dependencies [de5e40c]
765
+ - Updated dependencies [1a03af6]
766
+ - Updated dependencies [3e886eb]
767
+ - Updated dependencies [cfc675e]
768
+ - Updated dependencies [20df08c]
769
+ - Updated dependencies [1767124]
770
+ - Updated dependencies [8ecf5a6]
771
+ - Updated dependencies [af705b9]
772
+ - Updated dependencies [0502a7c]
773
+ - Updated dependencies [7b35e4b]
774
+ - Updated dependencies [8fb1295]
775
+ - Updated dependencies [e16ed2d]
776
+ - Updated dependencies [c6fd752]
777
+ - Updated dependencies [f9bbddb]
778
+ - Updated dependencies [dfd3705]
779
+ - Updated dependencies [c77108c]
780
+ - Updated dependencies [2735de6]
781
+ - Updated dependencies [697cda4]
782
+ - Updated dependencies [c19ac11]
783
+ - Updated dependencies [6dee2cb]
784
+ - Updated dependencies [e05f052]
785
+ - Updated dependencies [0502a7c]
786
+ - Updated dependencies [faad45e]
787
+ - Updated dependencies [09c6a17]
788
+ - Updated dependencies [c7cff19]
789
+ - Updated dependencies [ba73a02]
790
+ - Updated dependencies [cd09a7b]
791
+ - Updated dependencies [f1abf0e]
792
+ - Updated dependencies [f05b84e]
793
+ - Updated dependencies [9b4b952]
794
+ - Updated dependencies [341bfb5]
795
+ - Updated dependencies [2f947e4]
796
+ - Updated dependencies [7d46648]
797
+ - Updated dependencies [9b53d72]
798
+ - Updated dependencies [bb4aa25]
799
+ - Updated dependencies [75f1cdf]
800
+ - Updated dependencies [662bdf9]
801
+ - Updated dependencies [059a052]
802
+ - Updated dependencies [53642d4]
803
+ - Updated dependencies [8aae006]
804
+ - Updated dependencies [c6cfdf1]
805
+ - Updated dependencies [d147a13]
806
+ - Updated dependencies [c6aaed8]
807
+ - Updated dependencies [263f885]
808
+ - Updated dependencies [dc334da]
809
+ - @object-ui/components@17.0.0
810
+ - @object-ui/i18n@17.0.0
811
+ - @object-ui/fields@17.0.0
812
+ - @object-ui/react@17.0.0
813
+ - @object-ui/types@17.0.0
814
+ - @object-ui/core@17.0.0
815
+ - @object-ui/permissions@17.0.0
816
+
3
817
  ## 16.1.0
4
818
 
5
819
  ### Minor Changes