@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 +814 -0
- package/README.md +6 -0
- package/dist/DetailSection.d.ts.map +1 -1
- package/dist/DetailView.d.ts.map +1 -1
- package/dist/HeaderHighlight.d.ts.map +1 -1
- package/dist/InlineFieldInput.d.ts.map +1 -1
- package/dist/RecordActivityTimeline.d.ts +19 -2
- package/dist/RecordActivityTimeline.d.ts.map +1 -1
- package/dist/RecordMetaFooter.d.ts.map +1 -1
- package/dist/RelatedList.d.ts +11 -1
- package/dist/RelatedList.d.ts.map +1 -1
- package/dist/fieldEnrichment.d.ts +36 -0
- package/dist/fieldEnrichment.d.ts.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2052 -1483
- package/dist/index.umd.cjs +3 -3
- package/dist/renderers/record-activity.d.ts +10 -2
- package/dist/renderers/record-activity.d.ts.map +1 -1
- package/dist/renderers/record-details.d.ts.map +1 -1
- package/dist/renderers/record-quick-actions.d.ts.map +1 -1
- package/dist/renderers/record-related-list.d.ts.map +1 -1
- package/dist/renderers/recordActivityFeed.d.ts +93 -0
- package/dist/renderers/recordActivityFeed.d.ts.map +1 -0
- package/dist/synth/buildDefaultPageSchema.d.ts +37 -3
- package/dist/synth/buildDefaultPageSchema.d.ts.map +1 -1
- package/dist/useDetailTranslation.d.ts +10 -13
- package/dist/useDetailTranslation.d.ts.map +1 -1
- package/dist/useRecordEditable.d.ts +12 -0
- package/dist/useRecordEditable.d.ts.map +1 -0
- package/package.json +16 -16
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
|