@object-ui/i18n 16.0.0 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1270 -0
- package/dist/locales/ar.d.ts +827 -2
- package/dist/locales/ar.js +833 -7
- package/dist/locales/de.d.ts +825 -2
- package/dist/locales/de.js +831 -7
- package/dist/locales/en.d.ts +404 -17
- package/dist/locales/en.js +419 -19
- package/dist/locales/es.d.ts +825 -2
- package/dist/locales/es.js +831 -7
- package/dist/locales/fr.d.ts +827 -2
- package/dist/locales/fr.js +833 -7
- package/dist/locales/index.d.ts +7378 -90
- package/dist/locales/ja.d.ts +825 -2
- package/dist/locales/ja.js +831 -7
- package/dist/locales/ko.d.ts +825 -2
- package/dist/locales/ko.js +831 -7
- package/dist/locales/pt.d.ts +827 -2
- package/dist/locales/pt.js +833 -7
- package/dist/locales/ru.d.ts +827 -2
- package/dist/locales/ru.js +833 -7
- package/dist/locales/zh.d.ts +327 -18
- package/dist/locales/zh.js +329 -18
- package/dist/useObjectLabel.d.ts +240 -12
- package/dist/useObjectLabel.js +109 -44
- package/dist/useSafeTranslation.d.ts +2 -0
- package/dist/useSafeTranslation.js +7 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1275 @@
|
|
|
1
1
|
# @object-ui/i18n
|
|
2
2
|
|
|
3
|
+
## 17.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1cf0de7: fix(detail): finish the approval-lock story, and warn on silently stripped fields (framework#3794)
|
|
8
|
+
|
|
9
|
+
The Console reported record writability wrong in both directions during an
|
|
10
|
+
approval, so a user had nothing to go on: what they _could_ edit said "locked",
|
|
11
|
+
and what they _couldn't_ said "updated successfully".
|
|
12
|
+
|
|
13
|
+
**The lock band told the truth; the Edit button did not.** objectui#2902 split
|
|
14
|
+
the band into "in approval · editable" vs locked, but the header **Edit** CTA
|
|
15
|
+
still keyed off nothing at all — on a genuinely locked record it stayed live, so
|
|
16
|
+
the user opened the form, filled a screen, and got `RECORD_LOCKED` back on Save.
|
|
17
|
+
It is now `disabled` on a locked record: visible-but-off, with the band beside it
|
|
18
|
+
saying why. This is the LOCK, not the mere presence of an approval — a
|
|
19
|
+
`lockRecord: false` node keeps Edit live, which is the point of that setting.
|
|
20
|
+
|
|
21
|
+
**And the band could still re-lock itself.** `DetailView` OR-ed the record's own
|
|
22
|
+
`approval_status` mirror into `isLocked` unconditionally. That mirror is written
|
|
23
|
+
on submit by any flow configuring an `approvalStatusField`, _regardless of_
|
|
24
|
+
`lockRecord` — so on a `lockRecord: false` node the host correctly resolved "not
|
|
25
|
+
locked" from the request's `lock_record` while the mirror dragged the band back
|
|
26
|
+
to "Locked for approval", with the pencils live and saves landing underneath it.
|
|
27
|
+
The host is now authoritative whenever it threads `approvalPending`; the mirror
|
|
28
|
+
is consulted only for bare/legacy `DetailView` hosts that thread nothing, where
|
|
29
|
+
it still reads as locked (no node granularity — the safe direction).
|
|
30
|
+
|
|
31
|
+
Recall's tooltip no longer promises to unlock a record the node never locked
|
|
32
|
+
(`detail.cancelApprovalTooltipUnlocked`).
|
|
33
|
+
|
|
34
|
+
**Silently stripped fields now surface on the record form's save path.** The
|
|
35
|
+
adapter emitted a write-warning for `create`/`update` responses carrying
|
|
36
|
+
`droppedFields`, but not for `batchTransaction` — which is how the record form
|
|
37
|
+
saves a master-detail record, i.e. the one surface where a user actually edits a
|
|
38
|
+
`readonlyWhen`-locked field. `batchTransaction` now emits one warning per event,
|
|
39
|
+
resolving each back to its operation via the response's `index`.
|
|
40
|
+
|
|
41
|
+
The toast itself was hardcoded English and called every strip "read-only". It is
|
|
42
|
+
now localized (`detail.writeStripped*`, ten locales) and worded by reason:
|
|
43
|
+
`readonly_when` says the field is not editable _in this record's current state_,
|
|
44
|
+
which is what actually happened — the field is editable in other states and the
|
|
45
|
+
form rendered it as an ordinary input, so "read-only" sent the user hunting for a
|
|
46
|
+
permission problem that does not exist.
|
|
47
|
+
|
|
48
|
+
**And it stopped crying wolf.** `createObjectStackUserStateAdapter` hand-stamped
|
|
49
|
+
the server-managed `updated_at` on every recents/favorites write, which the
|
|
50
|
+
server strips and reports — so the console popped "Some fields were not saved"
|
|
51
|
+
about a field no user ever touched, on page loads, drowning the signal the toast
|
|
52
|
+
exists for. It no longer sends the column; the server stamps it anyway.
|
|
53
|
+
|
|
54
|
+
- d21794c: fix(list,i18n): a 400 from the server no longer reads as "check your connection"
|
|
55
|
+
|
|
56
|
+
`classifyLoadError` was written because a 403 rendered the same
|
|
57
|
+
"check your connection and try again" panel as a genuine outage — its own doc
|
|
58
|
+
comment says users "were told to debug their network when the server had
|
|
59
|
+
(correctly) denied them access." It made that distinction for 401 and 403 and
|
|
60
|
+
then sent **everything else**, including 4xx, to the network branch.
|
|
61
|
+
|
|
62
|
+
A **400** is the server saying it understood the request and will never accept
|
|
63
|
+
it. Retrying resends the identical bad request, so "check your connection and
|
|
64
|
+
try again" is advice that cannot work — the same mistake the function exists to
|
|
65
|
+
prevent, one status code over.
|
|
66
|
+
|
|
67
|
+
This became reachable from ordinary stored metadata with
|
|
68
|
+
objectstack-ai/objectstack#4121: a `$filter` array that is not a filter AST is
|
|
69
|
+
now rejected at the protocol with `400 INVALID_FILTER`, where it previously
|
|
70
|
+
reached a driver (and, for a lone `['and']`, silently returned every row). A
|
|
71
|
+
view saved with such a filter now answers 400 on every load.
|
|
72
|
+
|
|
73
|
+
Adds a fourth classification, `rejected`, for `status === 400` and for the
|
|
74
|
+
server's 400-class codes (`INVALID_FILTER`, `UNSUPPORTED_QUERY_PARAM`,
|
|
75
|
+
`INVALID_QUERY`). Its copy points at the filter rather than the network, and
|
|
76
|
+
says who can fix it when the view is saved that way. 403/401 keep priority, so a
|
|
77
|
+
permission denial can never read as a bad request — pinned by a test.
|
|
78
|
+
|
|
79
|
+
The two new strings are added to **all ten locale packs**, not just `en`: the
|
|
80
|
+
neighbouring panels are translated, and `fallbackLng: 'en'` would have rendered
|
|
81
|
+
this one in English beside them. The full-parity gate
|
|
82
|
+
(`all-locales-key-parity.test.ts`) caught the pack I missed.
|
|
83
|
+
|
|
84
|
+
Verified: 5 new tests — numeric status, error code without a status, a status
|
|
85
|
+
embedded in the message text, and the 403/401 ordering guard. Reverting the
|
|
86
|
+
branch fails four of them. `plugin-list` + `i18n`: **403 tests across 29 files**,
|
|
87
|
+
green.
|
|
88
|
+
|
|
89
|
+
- f8a95e5: fix(fields): the sharing-criteria builder stops calling an empty criteria "All records" (objectstack#3896)
|
|
90
|
+
|
|
91
|
+
`FilterConditionField` renders `sys_sharing_rule.criteria_json`. With no
|
|
92
|
+
criteria it displayed **"All records"**, and `filterGroupToMongo` carried a
|
|
93
|
+
matching `// empty = match all` comment. That was describing a bug as a
|
|
94
|
+
feature: a sharing rule with no predicate was stored as `criteria_json: null`
|
|
95
|
+
and evaluated as `find(object, { filter: {} })` under the system context —
|
|
96
|
+
every record of the object, granted to the recipient. `SharingRuleSchema` had
|
|
97
|
+
always forbidden the shape ("never seeded as a permissive match-all",
|
|
98
|
+
ADR-0049); the REST and data-API entries just never checked.
|
|
99
|
+
|
|
100
|
+
objectstack#3896 closes those entries: the server now refuses to save a rule
|
|
101
|
+
whose criteria would match everything, and one already stored shares nothing.
|
|
102
|
+
This is the renderer catching up.
|
|
103
|
+
|
|
104
|
+
- **The empty read-only state now says the rule shares nothing**, in
|
|
105
|
+
`destructive` styling — key renamed `fields.filterCondition.allRecords` →
|
|
106
|
+
`fields.filterCondition.noCriteria`, retranslated across all ten locales.
|
|
107
|
+
Nothing else read the old key.
|
|
108
|
+
- **A new `fields.filterCondition.criteriaRequired` hint** renders under the
|
|
109
|
+
builder (and the JSON editor) while the criteria is empty. The server's
|
|
110
|
+
rejection is precise but only arrives as a toast _after_ Save; this says it
|
|
111
|
+
while the admin is still looking at the empty builder.
|
|
112
|
+
- **`isMatchAllCriteria` is exported** — a client-side mirror of the server
|
|
113
|
+
predicate covering `{}`, `[]`, and the vacuous combinators (`{ $and: [] }`,
|
|
114
|
+
`{ $or: [{}] }`), conservative in the same direction. The server stays
|
|
115
|
+
authoritative; this only decides whether to show the hint.
|
|
116
|
+
|
|
117
|
+
Unparsable JSON keeps its own `invalidJson` message and does **not** also
|
|
118
|
+
collect the empty-criteria hint.
|
|
119
|
+
|
|
120
|
+
Note for anyone wiring this end-to-end: the Criteria field is not marked
|
|
121
|
+
`required` in the object metadata, deliberately — `sys_sharing_rule.criteria_json`
|
|
122
|
+
is nullable in deployed tenants, so `required: true` would only produce a
|
|
123
|
+
destructive `NOT NULL` migration that those nulls block. The invariant lives in
|
|
124
|
+
the server's write guards; this change makes the UI stop contradicting it.
|
|
125
|
+
|
|
126
|
+
### Patch Changes
|
|
127
|
+
|
|
128
|
+
- 752e18f: fix(console,app-shell): readable reassign hand-off + "System" label for svc:* audit actors — objectstack#4365 / objectstack#4366
|
|
129
|
+
|
|
130
|
+
- **Approvals inbox** (`ApprovalsInboxPage`): a reassign timeline entry now
|
|
131
|
+
renders "from A to B" from the structured
|
|
132
|
+
`reassign_from`/`reassign_to` fields (and their server-resolved
|
|
133
|
+
`*_name` companions) that objectstack#4365 added to
|
|
134
|
+
`sys_approval_action`, instead of relying on the old default comment that
|
|
135
|
+
baked two raw user ids into user-facing text. Legacy rows without the
|
|
136
|
+
structured fields keep the comment fallback. New i18n key
|
|
137
|
+
`approvalsInbox.reassignFromTo` across all ten locales.
|
|
138
|
+
- **Record history** (`RecordDetailView`): an audit row attributed to a
|
|
139
|
+
service principal (`svc:*` on the `actor` column — e.g. a
|
|
140
|
+
`runAs:'system'` flow's `svc:flow:<name>` label from objectstack#4366) now
|
|
141
|
+
renders the localized "System" label instead of the raw principal string;
|
|
142
|
+
the raw value stays on the entry for tooling.
|
|
143
|
+
|
|
144
|
+
- c785740: fix(detail): record Attachments become their own tab (with count badge) and their copy is translated — objectstack#4358
|
|
145
|
+
|
|
146
|
+
Two defects on `enable.files: true` record detail pages:
|
|
147
|
+
|
|
148
|
+
1. **Buried placement.** `RecordDetailView` appended `RecordAttachmentsPanel`
|
|
149
|
+
AFTER the schema-rendered page tree, whose synthesized default embeds
|
|
150
|
+
`record:discussion` as the last main component — so the panel always
|
|
151
|
+
landed below an ever-growing feed timeline, undiscoverable without
|
|
152
|
+
scrolling to the very bottom, with no metadata knob to move it.
|
|
153
|
+
|
|
154
|
+
`buildDefaultTabs` now emits a peer **Attachments** tab (a new
|
|
155
|
+
`record:attachments` node rendered by an app-shell registration wrapping
|
|
156
|
+
the existing panel via RecordContext) between Related and
|
|
157
|
+
Activity/History. `PageTabsRenderer` derives the tab's count badge from a
|
|
158
|
+
`sys_attachment` probe scoped to `(parent_object, parent_id)`, riding the
|
|
159
|
+
same RelatedCountStore cache/invalidation bus as related-list badges — so
|
|
160
|
+
uploads and deletes update the badge live. A `hideAttachments` synthesizer
|
|
161
|
+
option suppresses the tab; RecordDetailView keeps its legacy bottom append
|
|
162
|
+
only as the fallback for authored pages without the node
|
|
163
|
+
(`hasExplicitAttachments`).
|
|
164
|
+
|
|
165
|
+
2. **Untranslated copy.** The panel's eleven `detail.*` keys (`attachments`,
|
|
166
|
+
`uploadAttachment`, `loadingAttachments`, `noAttachments`,
|
|
167
|
+
`downloadAttachment`, `deleteAttachment`, and the five
|
|
168
|
+
`attachment*Denied/Required` friendly errors) existed only as inline
|
|
169
|
+
English `defaultValue`s — no locale bundle carried them, so non-English
|
|
170
|
+
consoles always showed English. All ten locales now define them; the tab
|
|
171
|
+
label rides the existing well-known-label dictionary (→ 附件 etc.).
|
|
172
|
+
|
|
173
|
+
- d61efd1: fix(grid): a bulk action's `visible` is evaluated per selected record — objectui#3067
|
|
174
|
+
|
|
175
|
+
The selection bar evaluated a def's `visible` against the ambient scope with no
|
|
176
|
+
record bound. That does not fail open, it answers wrongly: with no `record` in
|
|
177
|
+
scope the lenient evaluator returned `true` for **every** row-scoped predicate,
|
|
178
|
+
including the ones that should be false — `${record.done}` and
|
|
179
|
+
`${record.owner == user.id}` both came back `true`. An authored gate was not
|
|
180
|
+
weakened, it was inverted for half its inputs, and nothing distinguished that
|
|
181
|
+
from a real verdict.
|
|
182
|
+
|
|
183
|
+
`visible` is now evaluated **once per selected record, with that record in
|
|
184
|
+
scope**, fail-closed per record and warning once on a fault — the same contract
|
|
185
|
+
the row kebab uses. One evaluation answers both questions:
|
|
186
|
+
|
|
187
|
+
- **Is the button offered?** When at least one selected record passes. A
|
|
188
|
+
record-free predicate (`features.x`, `current_user.y`) returns the same
|
|
189
|
+
verdict for every row, so it still behaves as a plain button-level gate — no
|
|
190
|
+
syntactic sniffing for `record` references is involved.
|
|
191
|
+
- **Which records does it run on?** The ones that passed. The confirm step
|
|
192
|
+
states how many were skipped, so a run over fewer records than the user
|
|
193
|
+
ticked says so instead of quietly shrinking the selection.
|
|
194
|
+
|
|
195
|
+
Eligibility is re-applied to the EXPANDED set after "select all N matching",
|
|
196
|
+
not just the page selection the button could see.
|
|
197
|
+
|
|
198
|
+
The mechanism predates objectui#3002, but only inline-authored
|
|
199
|
+
`bulkActionDefs[].visible` used to reach it — written by authors who knew there
|
|
200
|
+
was no record. #3031 began promoting object actions into the bar, and their
|
|
201
|
+
`visible` is typically written for a row/record surface, which is what put
|
|
202
|
+
row-scoped predicates in front of a record-free evaluation.
|
|
203
|
+
|
|
204
|
+
- 9eb932b: fix(console): three real-user console failures — 403 blamed on the network, ⌘K search capped at 8 objects, nav gating fields inert
|
|
205
|
+
|
|
206
|
+
1. **List error panel classifies the failure** (`plugin-list`, `i18n`): a 403/401 from the data source used to render the same "check your connection" copy as a genuine outage, sending users to debug their network while the server was correctly denying access. The panel now classifies by `httpStatus`/`status`/`statusCode`, the `PERMISSION_DENIED`/`UNAUTHORIZED` error codes, or an `HTTP <status>` message prefix, and renders dedicated permission-denied / sign-in-required copy (all nine locales).
|
|
207
|
+
|
|
208
|
+
2. **⌘K / full-page search scope is no longer truncated** (`react`): `maxObjectsQueried` caps the per-object fanout fallback, not the search scope — it used to slice the candidate pool itself, so the `objects` whitelist sent to the platform's `/api/v1/search` only ever named the first 8 nav objects. Which sidebar group came first decided which records were findable; everything later in the nav was unsearchable no matter what the user typed.
|
|
209
|
+
|
|
210
|
+
3. **Nav gating fields finally gate** (`app-shell`): `evaluateVisibility` only evaluated `${…}` template strings, so the `{ dialect: 'cel', source }` envelopes the spec normalizes every authored `visible` predicate into fell through to a blanket "visible" — a constant-false predicate still rendered for everyone. It now delegates to `ExpressionEvaluator.evaluateCondition`, which routes CEL envelopes to the canonical `@objectstack/formula` engine. And the sidebars' `requiredPermissions` check treats a bare name as an ADR-0066 system capability (union of the user's permission-set `systemPermissions` from `/me/permissions`) — the same subset rule the server applies to `AppSchema.requiredPermissions` — instead of misreading it as `can(<name>, 'read')`, which had degraded `requiredPermissions` into a hide-from-everyone switch (admins included). The `object:action` form and the legacy object-read fallback keep working.
|
|
211
|
+
|
|
212
|
+
- 3cb9646: fix(app-shell,i18n): record forms no longer render the developer-voiced default subtitle
|
|
213
|
+
|
|
214
|
+
Every create/edit record form (both the console dialog in `AppContent` and the
|
|
215
|
+
full-page `RecordFormPage`) hardcoded a platform default description under the
|
|
216
|
+
title: "Add a new {{object}} to your database." / "Update details for
|
|
217
|
+
{{object}}" (zh: 「向数据库添加新的{{object}}。」/「更新{{object}}的详情」).
|
|
218
|
+
|
|
219
|
+
The copy is developer-tooling voice leaking into end-user business apps — a
|
|
220
|
+
scheduling clerk filling in a 排班计划 has no business being told about "the
|
|
221
|
+
database", and the phrasing came straight from admin-panel boilerplate. The
|
|
222
|
+
line carried no information the form title didn't already have, and neither
|
|
223
|
+
call site let a form view override it.
|
|
224
|
+
|
|
225
|
+
The default subtitle is now gone: both call sites stop passing `description`,
|
|
226
|
+
and the unused `form.createDescription` / `form.editDescription` keys are
|
|
227
|
+
removed from all ten locale bundles (the `workspace.createDescription` key is
|
|
228
|
+
unrelated and stays).
|
|
229
|
+
|
|
230
|
+
- b5b97e2: fix(types,layout): nav item type `component` joins `NavigationItemType` and its zod enum — objectui#2918
|
|
231
|
+
|
|
232
|
+
The renderers have carried a full `type: 'component'` implementation (Phase 3b:
|
|
233
|
+
`componentRef` colon-split to `/component/<ns>/<name>`, `params` serialised as
|
|
234
|
+
querystring, `metadata:*` special-cases) — but the vocabulary never gained the
|
|
235
|
+
member, and `@objectstack/spec` has had `ComponentNavItem` all along. The zod
|
|
236
|
+
enum was the part that bit: `NavigationItemTypeSchema` rejected
|
|
237
|
+
`type: 'component'` at validation time, so authors could not declare one and
|
|
238
|
+
the renderer half was unreachable — dead on arrival rather than dead code.
|
|
239
|
+
|
|
240
|
+
- `NavigationItemType` and `NavigationItemTypeSchema` gain `'component'`;
|
|
241
|
+
`NavigationItem` gains the fields the renderer consumes, `componentRef` and
|
|
242
|
+
`params` (also used by `type: 'page'`), mirroring spec's `ComponentNavItem` —
|
|
243
|
+
declared in zod too, so parse no longer strips them.
|
|
244
|
+
- The `(item as any).componentRef` / `params` casts in `NavigationRenderer`
|
|
245
|
+
and `AppSchemaRenderer` become typed access.
|
|
246
|
+
- `NavigationDesigner`'s exhaustive type-meta map gains a `component` badge
|
|
247
|
+
(new `appDesigner.navTypeComponent` key in all 10 locales).
|
|
248
|
+
- `@object-ui/layout` gains `type-check` (src + tests) with the #2915 `paths`
|
|
249
|
+
override; its DEBT entry in `check-type-check-coverage.mjs` is deleted.
|
|
250
|
+
|
|
251
|
+
- 2d5d594: fix(list,detail): sorting a lookup column no longer orders by an invisible key — #3096
|
|
252
|
+
|
|
253
|
+
A relational column (`lookup` / `master_detail` / `user` / `tree`) never holds
|
|
254
|
+
the string its cell shows: it holds the `$expand`-ed record, or a raw foreign-key
|
|
255
|
+
id whose label was resolved separately. Every sort path took that raw value as
|
|
256
|
+
its key, so the column of names came back in an order with no relation to the
|
|
257
|
+
names — sorting looked broken, with nothing saying the key was something else.
|
|
258
|
+
|
|
259
|
+
The two halves are fixed differently, because they can order by different things:
|
|
260
|
+
|
|
261
|
+
- **Client-side sorts** (grid column headers, any `data-table`, a non-windowed
|
|
262
|
+
related list) now key off the label the cell renders, via the new
|
|
263
|
+
`getSortValue` / `compareSortValues` in `@object-ui/core` — which resolves an
|
|
264
|
+
expanded record through `getRecordDisplayName` (ADR-0079), so the sort key and
|
|
265
|
+
the lookup cell agree on which field names a record. This replaces two broken
|
|
266
|
+
comparators: `a[col] < b[col]` is always false between two objects (the
|
|
267
|
+
comparator collapsed to a constant and permuted the rows), and
|
|
268
|
+
`String(a[col])` is `"[object Object]"` (every row compared equal, so the sort
|
|
269
|
+
silently did nothing).
|
|
270
|
+
- **Server `$orderby` sorts** cannot be fixed here — the key is the stored id by
|
|
271
|
+
construction, and `objectstack#4256` settled that no relation join is coming.
|
|
272
|
+
So those entry points stop offering the illusion: the ListView toolbar sort
|
|
273
|
+
picker withholds relational fields and explains why (pointing at a formula
|
|
274
|
+
field as the supported way to sort by a related name), and a windowed related
|
|
275
|
+
list renders no sort button for them.
|
|
276
|
+
|
|
277
|
+
A relational field the view's CURRENT sort already uses stays listed, labelled
|
|
278
|
+
`(by ID)`, so view metadata authored or saved with such a sort round-trips
|
|
279
|
+
instead of rendering a blank row and losing the sort on the next edit.
|
|
280
|
+
|
|
281
|
+
## 17.0.0
|
|
282
|
+
|
|
283
|
+
### Minor Changes
|
|
284
|
+
|
|
285
|
+
- 952b978: fix(detail): the approval band honors the node's `lockRecord` instead of assuming every approval locks (#2902)
|
|
286
|
+
|
|
287
|
+
A record detail page treated "a pending approval request exists" as "this
|
|
288
|
+
record is locked". An approval node declares `lockRecord` (default `true`), and
|
|
289
|
+
on `lockRecord: false` the server keeps accepting writes for the whole time
|
|
290
|
+
that node waits — so the console was asserting a lock the backend did not
|
|
291
|
+
enforce.
|
|
292
|
+
|
|
293
|
+
The label was the smaller half of it. The same conflated signal fed `canEdit`,
|
|
294
|
+
so the record-level inline-edit session was suppressed too: no pencils,
|
|
295
|
+
`enter()` a no-op. On a single-approver step — a department head or plant
|
|
296
|
+
manager, exactly the case `lockRecord: false` exists for, where the approver is
|
|
297
|
+
meant to fill in the missing detail before deciding — the capability was
|
|
298
|
+
unreachable from the UI. And a flow chaining nodes with different policies drew
|
|
299
|
+
one identical band for "edit freely" and "the server will reject your save with
|
|
300
|
+
`RECORD_LOCKED`", so the two states were indistinguishable until Save failed.
|
|
301
|
+
|
|
302
|
+
Approval state is now two signals:
|
|
303
|
+
|
|
304
|
+
- **`approvalPending`** — an approval is running. Drives the band and the recall
|
|
305
|
+
button, both meaningful whether or not the record is editable.
|
|
306
|
+
- **`locked`** — that approval also forbids edits, from the pending node's
|
|
307
|
+
`lock_record` (framework#3814, read off the same `node_config_json` snapshot
|
|
308
|
+
the server's record-lock hook reads).
|
|
309
|
+
|
|
310
|
+
The band renders two states: amber lock + "Locked for approval", or sky clock +
|
|
311
|
+
"In approval · editable", each with its own tooltip. Recall moved out of the
|
|
312
|
+
locked branch — an editable pending approval is just as recallable. Inline
|
|
313
|
+
editing stays live in the editable state.
|
|
314
|
+
|
|
315
|
+
`InlineEditProvider` takes a new optional `approvalPending` prop, defaulting to
|
|
316
|
+
`locked`, so a host that threads only `locked` renders exactly as before. The
|
|
317
|
+
record's `approval_status` field remains the fallback for backends with no
|
|
318
|
+
approvals API; it carries no node granularity, so it still reads as locked — as
|
|
319
|
+
does a pending request from a backend too old to report the policy.
|
|
320
|
+
|
|
321
|
+
New `detail.approvalPendingEditable` / `detail.approvalPendingTooltip` keys are
|
|
322
|
+
translated in all ten locales.
|
|
323
|
+
|
|
324
|
+
- 2735de6: feat: render the server's effective API operation set (#3391 PR-4)
|
|
325
|
+
|
|
326
|
+
The frontend now consumes the per-object **effective API operation set** the
|
|
327
|
+
server resolves (from `/me/permissions` `apiOperations`, framework #3391) —
|
|
328
|
+
never the raw `apiMethods` — so Import/Export/New/Edit/Delete buttons match what
|
|
329
|
+
the server will actually admit, and a 405 import refusal shows a dedicated
|
|
330
|
+
message instead of silently falling back.
|
|
331
|
+
|
|
332
|
+
- **core** `resolveCrudAffordances(obj, effectiveApiOperations?)` — new optional
|
|
333
|
+
second argument intersects each affordance bit with its API operation
|
|
334
|
+
(create/import→create/import, edit→update, delete→delete, exportCsv→export).
|
|
335
|
+
Omitting it (old backend / no effective set) leaves affordances unchanged.
|
|
336
|
+
- **permissions** — `/me/permissions` response carries per-object
|
|
337
|
+
`apiOperations`; `PermissionContextValue.getObjectApiOperations(object)`
|
|
338
|
+
exposes it (undefined when absent → callers keep current behavior); `check()`
|
|
339
|
+
maps `import→allowCreate`, `export→allowRead`.
|
|
340
|
+
- **app-shell** `ObjectView` intersects its toolbar affordances with the object's
|
|
341
|
+
effective operations (Import); the platform-admin identity-import bypass is
|
|
342
|
+
unaffected.
|
|
343
|
+
- **plugin-list** `ListView` / **plugin-grid** `ObjectGrid` gate the Export
|
|
344
|
+
button (and export handler) on effective `export`; `plugin-grid` gains the
|
|
345
|
+
`@object-ui/permissions` workspace dependency.
|
|
346
|
+
- **plugin-grid** `ImportWizard` — a 405 / `OBJECT_API_METHOD_NOT_ALLOWED`
|
|
347
|
+
import refusal is detected by a new `isImportNotAllowed` predicate at every
|
|
348
|
+
catch site (async, sync, dry-run) and STOPS with a dedicated
|
|
349
|
+
`grid.import.notAllowed` message (10 locales + fallback dict) — it never falls
|
|
350
|
+
back to the sync/legacy path (which 405s too), distinct from the 404
|
|
351
|
+
route-absent fallback.
|
|
352
|
+
|
|
353
|
+
Backward-compatible: a missing effective set (unrestricted object, older
|
|
354
|
+
backend, or no permission provider) preserves the current default-allow
|
|
355
|
+
behavior everywhere.
|
|
356
|
+
|
|
357
|
+
### Patch Changes
|
|
358
|
+
|
|
359
|
+
- 0b3be01: fix(app-shell): give inline `lookup` action params a real record picker (#3405)
|
|
360
|
+
|
|
361
|
+
An action parameter declared inline as `{ name: 'inspector', type: 'lookup',
|
|
362
|
+
reference: 'sys_user' }` always rendered as a plain text input asking the user
|
|
363
|
+
to paste a record id (UUID) — a supervisor assigning an inspector had to go
|
|
364
|
+
find that person's UUID by hand, while the same reference field picks records
|
|
365
|
+
by name in the create/edit dialog.
|
|
366
|
+
|
|
367
|
+
`paramToField()` degrades a picker param to text when it has no `referenceTo`
|
|
368
|
+
target, and `referenceTo` was only ever populated on the field-backed branch of
|
|
369
|
+
`resolveActionParams()`. The inline branch dropped the authored `reference`
|
|
370
|
+
key entirely (as did the spec schema, which stripped it as unknown), so an
|
|
371
|
+
inline picker could never reach `<LookupField>` no matter how it was authored.
|
|
372
|
+
|
|
373
|
+
- `resolveActionParam()` now maps an inline `reference` onto `referenceTo` — on
|
|
374
|
+
the inline branch, on the missing-field fallback branch, and as an override
|
|
375
|
+
on the field-backed branch (matching how every other inline value overrides
|
|
376
|
+
the resolved field).
|
|
377
|
+
- The text degradation now warns in dev naming the offending param, since with
|
|
378
|
+
`@objectstack/spec` rejecting a targetless inline picker at parse time it
|
|
379
|
+
means the metadata is broken, not merely partial.
|
|
380
|
+
- The fallback's placeholder and help text no longer claim "a picker is coming
|
|
381
|
+
soon" — the picker has shipped, and the message now says the parameter has no
|
|
382
|
+
reference object configured. Updated across all 10 locales.
|
|
383
|
+
|
|
384
|
+
- 3c4d935: fix(i18n): compose the AI-model diagnostics summary client-side instead of rendering the server's English string (objectui#2886)
|
|
385
|
+
|
|
386
|
+
`CloudAiModelStatus` rendered `report.summary` verbatim — the most prominent
|
|
387
|
+
line on the panel, in English for every locale.
|
|
388
|
+
|
|
389
|
+
Reading `objectstack-ai/cloud` settled how to fix it. The server **cannot**
|
|
390
|
+
localize that string as currently built:
|
|
391
|
+
|
|
392
|
+
- `service-ai/src/effective-model.ts:117` assembles it as a hard-coded English
|
|
393
|
+
template literal, with no locale parameter;
|
|
394
|
+
- `service-ai/src/routes/ai-routes.ts:395` declares `handler: async () => …` —
|
|
395
|
+
it takes **no request argument**, so it cannot read `Accept-Language` even
|
|
396
|
+
though `createAuthenticatedFetch` has been sending it since objectui#1319.
|
|
397
|
+
|
|
398
|
+
But no server change is needed, because every ingredient of the sentence is
|
|
399
|
+
already in the structured payload: `conversational.model`,
|
|
400
|
+
`conversational.source`, `structured.model`, `structured.pinned`, and
|
|
401
|
+
`routing.{free,paid}`. The issue proposed "return structured data instead of a
|
|
402
|
+
sentence" as the better fix — the server was already doing that; the client
|
|
403
|
+
just wasn't using it.
|
|
404
|
+
|
|
405
|
+
The panel now composes the line from those fields. `sourceLabel()` already
|
|
406
|
+
produced exactly the two clauses the server hand-rolls — "pinned by X" /
|
|
407
|
+
"code default (no env override)", and "same as build/ask" for an unpinned
|
|
408
|
+
structured model — so no new source vocabulary was required.
|
|
409
|
+
|
|
410
|
+
**A dropped diagnostic, not just untranslated text.** The client's
|
|
411
|
+
`EffectiveModelReport` never declared `routing`, which the server has always
|
|
412
|
+
sent conditionally. Its only appearance anywhere was inside the English summary,
|
|
413
|
+
so non-English admins could not see the plan→model routing policy **at all**.
|
|
414
|
+
It is now declared and surfaced.
|
|
415
|
+
|
|
416
|
+
Also fixed: `attributeSource` emits the bare token `'unknown'` when the adapter
|
|
417
|
+
cannot report a model, and `sourceLabel` fell through to rendering it raw.
|
|
418
|
+
|
|
419
|
+
Four keys added to all ten packs (`summary`, `summaryRouting`, `modelUnknown`,
|
|
420
|
+
`sourceUnknown`), so the full-parity guard from objectui#2909 stays green.
|
|
421
|
+
|
|
422
|
+
The panel had **no test coverage at all**; it now has five, mutation-tested by
|
|
423
|
+
restoring `<p>{report.summary}</p>` — which fails four of them.
|
|
424
|
+
|
|
425
|
+
- 4b60d2d: fix(console): make the approval timeline attachment chip show its name and open (#2820)
|
|
426
|
+
|
|
427
|
+
A decision attachment in the approval inbox timeline (审批动态) rendered a
|
|
428
|
+
nameless "附件" chip that did nothing when clicked. Three separate bugs:
|
|
429
|
+
|
|
430
|
+
- **No filename.** The chip resolved its label by fetching `/data/sys_file/{id}`
|
|
431
|
+
— a system object a regular approver cannot read — and silently fell back to a
|
|
432
|
+
generic label when that was denied. The name now comes from the attachment
|
|
433
|
+
descriptor the server returns (framework #3266), so no `sys_file` access is
|
|
434
|
+
needed and the real filename shows for every approver.
|
|
435
|
+
- **Dead click.** `openAttachment` called `window.open` _after_ an `await`, so
|
|
436
|
+
it was no longer a user gesture and the browser blocked the popup. It now opens
|
|
437
|
+
the tab synchronously up front, then points it at the signed URL once fetched.
|
|
438
|
+
- **Wrong origin.** The signed URL from the local storage adapter is
|
|
439
|
+
server-relative; `window.open` resolved it against the console origin. It is
|
|
440
|
+
now resolved against the API origin.
|
|
441
|
+
- Every open failure was swallowed silently. The user now gets a toast on
|
|
442
|
+
failure — new `approvalsInbox.attachmentOpenFailed` string across all 10
|
|
443
|
+
locales.
|
|
444
|
+
|
|
445
|
+
- de5e40c: fix(approvals): Approval Center UX pass — badge nowrap, approve confirm, decision progress bar, localized declared actions (#2762)
|
|
446
|
+
|
|
447
|
+
- **Badge no longer stacks CJK text vertically (P0-1)** — `Badge` gains
|
|
448
|
+
`whitespace-nowrap` in its base variants (a badge is a single-line pill by
|
|
449
|
+
definition), and the inbox 状态 column gets a minimum width, so 待审批 can
|
|
450
|
+
never render as 待/审/批.
|
|
451
|
+
- **Quick Approve now confirms (P0-2)** — the row's right-edge ✓, the mobile
|
|
452
|
+
card button and the `a` keyboard shortcut all route through a confirmation
|
|
453
|
+
dialog before executing, mirroring the Reject flow; an irreversible decision
|
|
454
|
+
can no longer fire on a stray click.
|
|
455
|
+
- **Decision progress is visualized (P1-1)** — the drawer renders a segmented
|
|
456
|
+
progress bar (ARIA `progressbar`) for `decision_progress`, per-group chips
|
|
457
|
+
get an explicit unsatisfied ○ state next to the satisfied ✓, the eligible
|
|
458
|
+
approver count is spelled out, and the drawer pager now reads
|
|
459
|
+
"Request N of M" so it can't be misread as approval progress.
|
|
460
|
+
- **Declared action labels localize (P0-3)** — `DeclaredActionsBar` resolves
|
|
461
|
+
label / confirmText / successMessage through the `_actions.<name>.*`
|
|
462
|
+
translation convention (metadata literals as fallback), matching
|
|
463
|
+
ObjectView/RecordDetailView; with the `@objectstack/plugin-approvals`
|
|
464
|
+
bundle, the drawer shows 通过 / 拒绝 / 转签 instead of English in a zh-CN
|
|
465
|
+
workspace. New `approvalsInbox` keys shipped in all ten locales.
|
|
466
|
+
|
|
467
|
+
- 1a03af6: fix(approvals): Approval Center triage + drawer readability pass (#2762 P1-2/P1-3/P1-4/P1-5/P2)
|
|
468
|
+
|
|
469
|
+
- **Decision-relevant data in the queue (P1-3)** — list rows and mobile cards
|
|
470
|
+
now surface the request's amount/total inline (detected from the snapshot,
|
|
471
|
+
preferring the server-formatted `payload_display` value), so a reviewer can
|
|
472
|
+
triage without opening each request. A sort control adds "Oldest first" and
|
|
473
|
+
"Amount (high→low)" alongside the default newest-first.
|
|
474
|
+
- **Empty applicant column (P1-4)** — flow-/system-initiated requests (no human
|
|
475
|
+
submitter) now read "Flow-initiated" with a workflow icon instead of a bare
|
|
476
|
+
person icon + "—", in the desktop table, mobile card, and drawer.
|
|
477
|
+
- **Approver chips deduped (P1-2)** — a person filling more than one approver
|
|
478
|
+
slot rendered as N identical "Waiting on" chips; they collapse to one chip
|
|
479
|
+
with a ×N count, the tooltip keeping every underlying id.
|
|
480
|
+
- **Action hierarchy (P1-5)** — `DeclaredActionsBar` maps the spec action
|
|
481
|
+
`variant` enum onto the Button variants (`primary` → filled default,
|
|
482
|
+
`danger` → destructive), so the drawer's Approve stands out and Reject reads
|
|
483
|
+
as destructive once `@objectstack/plugin-approvals` declares them.
|
|
484
|
+
- **Label polish (P2)** — `owner_id`-style resolved lookup keys render as
|
|
485
|
+
"Owner", not the awkward "Owner Id", in the drawer summary.
|
|
486
|
+
|
|
487
|
+
New `approvalsInbox` keys (`flowOrigin`, `sortBy`/`sortRecent`/`sortOldest`/
|
|
488
|
+
`sortAmount`) added to all ten locales.
|
|
489
|
+
|
|
490
|
+
- 3e886eb: fix(i18n): localize FileField upload widget + approvals snapshot field labels
|
|
491
|
+
|
|
492
|
+
- `FileField` (the shared upload widget) hard-coded every visible string
|
|
493
|
+
("Drag & drop files here", "or click to browse", "Take photo", "Uploading…",
|
|
494
|
+
size/upload validation messages, …). They now route through
|
|
495
|
+
`useObjectTranslation` with new `fields.file.*` keys, translated across all
|
|
496
|
+
10 locale bundles. This is why the approvals Approve/Reject dialog's
|
|
497
|
+
attachment dropzone was English in a Chinese console.
|
|
498
|
+
- The approvals inbox record-snapshot summary title-cased raw machine keys
|
|
499
|
+
instead of the target object's field labels. It now consumes the
|
|
500
|
+
server-sent `payload_labels` in `payloadSummary`/`decisionAmountEntry`,
|
|
501
|
+
falling back to the prettified key when absent; `approvalsApi`'s row type
|
|
502
|
+
gains `payload_labels`.
|
|
503
|
+
|
|
504
|
+
- cfc675e: fix(i18n): unconditional Chinese in the chatbot confirm card and the field inspector (objectui#2884, objectui#2885)
|
|
505
|
+
|
|
506
|
+
Two issues split out of the objectui#2871 survey because neither is a language
|
|
507
|
+
_branch_ — both are copy that renders in Chinese for every user regardless of
|
|
508
|
+
locale.
|
|
509
|
+
|
|
510
|
+
**objectui#2884 — the confirm-before-change card.** Heading, buttons, hint and
|
|
511
|
+
the verb column of each change row were Chinese literals, so an English user
|
|
512
|
+
read the whole confirm gate in Chinese. They now follow the same
|
|
513
|
+
prop-with-English-default convention the plan card already uses
|
|
514
|
+
(`changesTitleLabel`, `changesConfirmLabel`, `changeVerbLabels`, …), with the
|
|
515
|
+
console passing translated values from `console.ai.*`.
|
|
516
|
+
|
|
517
|
+
The serious half was the outbound message. Clicking Confirm sent
|
|
518
|
+
`'确认修改,应用你刚才提议的改动。'` unconditionally — an English user's click
|
|
519
|
+
told the agent, in Chinese, to apply the changes, and the agent answered in
|
|
520
|
+
Chinese for the rest of the thread. That message now routes through the same
|
|
521
|
+
`convZh` (conversation-language) switch as `planApproveMessage`, so it matches
|
|
522
|
+
the language actually being spoken rather than the UI or a hard-coded literal.
|
|
523
|
+
|
|
524
|
+
Note this is deliberately _not_ "always send English": the repo already decided
|
|
525
|
+
outbound agent text follows the CONVERSATION, and the cloud confirm gate
|
|
526
|
+
(`service-ai-studio` `confirm-gate.ts` `APPROVAL_RE`) matches on approval
|
|
527
|
+
keywords. The Chinese string is unchanged, so that path is byte-for-byte what
|
|
528
|
+
the gate already accepted; `i18n.test.ts` now pins it against the mirrored gate
|
|
529
|
+
regex alongside the two plan messages.
|
|
530
|
+
|
|
531
|
+
Also in this component: the error banner's `Response failed` / `Details` /
|
|
532
|
+
`Retry` were hard-coded English, and both it and the quota banner used a bare
|
|
533
|
+
`t(key)` that renders the raw key when the chat is mounted without an
|
|
534
|
+
`I18nProvider`. Both now use `useSafeTranslate`, so they degrade to English
|
|
535
|
+
instead of to `chatbotError.title`. The `「…」` corner brackets around the
|
|
536
|
+
target-app name are now neutral quotes.
|
|
537
|
+
|
|
538
|
+
**objectui#2885 — the draft-field suffix.** `ObjectFieldInspector` appended a
|
|
539
|
+
bare `(草稿)` to draft objects in the lookup picker — the only Chinese literal
|
|
540
|
+
in a 1500-line file where the other 101 strings all go through `t(key, locale)`.
|
|
541
|
+
It now reads `engine.inspector.draftSuffix` from the Studio catalog.
|
|
542
|
+
|
|
543
|
+
The 18 new keys were added to all ten locale packs, so the objectui#2872 part
|
|
544
|
+
(a) gap held at 469/471 rather than widening.
|
|
545
|
+
|
|
546
|
+
- 20df08c: fix(cloud-connection): localize the Cloud Connection panel (objectstack#3589 follow-up)
|
|
547
|
+
|
|
548
|
+
`CloudConnectionPanel` — the `cloud-connection:panel` SDUI widget that is the
|
|
549
|
+
entire body of the Cloud Connection Setup page — had no i18n at all: no
|
|
550
|
+
`@object-ui/i18n` import, and no `cloudConnection` namespace in any of the ten
|
|
551
|
+
built-in locale packs. Its siblings on neighbouring pages
|
|
552
|
+
(`marketplace:installed-list`, `mcp:connect-agent`) were already fully
|
|
553
|
+
localized, so this one page rendered a translated header above an English body
|
|
554
|
+
once the framework-side `page:header` resolution landed.
|
|
555
|
+
|
|
556
|
+
- New `cloudConnection` namespace in all ten packs (en, zh, ja, ko, de, fr, es,
|
|
557
|
+
pt, ru, ar), matching the coverage its sibling namespaces already had. Covers
|
|
558
|
+
every phase of the device-code flow: checking, error + retry, waiting
|
|
559
|
+
(approval prompt, user code, copy), bound (connection detail labels), and
|
|
560
|
+
unbound (call to action).
|
|
561
|
+
- The three hard-coded failure messages (expired request, bind failure, device
|
|
562
|
+
code request failure) are translated where they are raised, not where they
|
|
563
|
+
are rendered, since they are stored in component state.
|
|
564
|
+
- The "code is pre-filled…" line was one sentence stitched together across JSX
|
|
565
|
+
with a conditional tail and a bare `'.'`. It is now two self-contained
|
|
566
|
+
strings, so a translator never receives a dangling clause whose word order
|
|
567
|
+
they cannot change.
|
|
568
|
+
- The `bound_at` timestamp now formats with the active UI language rather than
|
|
569
|
+
the browser default, matching the surrounding copy.
|
|
570
|
+
|
|
571
|
+
Also adds a locale-parity test asserting the `cloudConnection` key set is
|
|
572
|
+
identical across all ten packs — partial coverage degrades quietly, because
|
|
573
|
+
i18next falls back to `en` and the result merely looks half-translated.
|
|
574
|
+
|
|
575
|
+
- af705b9: feat(i18n): complete the locale backfill — all ten packs reach full key parity (objectui#2872)
|
|
576
|
+
|
|
577
|
+
Translates the remaining **275 keys × 8 packs = 2,200 strings**, closing
|
|
578
|
+
objectui#2872. The largest namespaces are `grid` (101, mostly the import
|
|
579
|
+
wizard), `gantt` (58) and `dashboard` (25), plus a long tail across `list`,
|
|
580
|
+
`auth`, `fields`, `marketplace`, `capability` and nine others.
|
|
581
|
+
|
|
582
|
+
Every pack is now at parity with `en`: **2,495 of 2,499 keys**, zero keys that
|
|
583
|
+
`en` lacks. The four-key remainder is the outbound-message set, absent by
|
|
584
|
+
design so `t()` falls through to English and the cloud confirm gate keeps
|
|
585
|
+
recognising it — `outbound-agent-messages.test.ts` owns that invariant.
|
|
586
|
+
|
|
587
|
+
**P3 is now enforceable.** `high-frequency-namespace-parity.test.ts` was scoped
|
|
588
|
+
to four namespaces because full parity would have been a permanently red build.
|
|
589
|
+
That restriction is obsolete, so it is replaced by
|
|
590
|
+
`all-locales-key-parity.test.ts`, which asserts:
|
|
591
|
+
|
|
592
|
+
- every pack defines every `en` key;
|
|
593
|
+
- no pack defines a key `en` lacks (objectui#2872 part b was 74 keys of exactly
|
|
594
|
+
this, hidden behind a component-private fallback);
|
|
595
|
+
- **placeholders match `en` per string** — both `{{count}}` and the single-brace
|
|
596
|
+
`{count}` form, which two `gantt.autoScheduleDlg.*` keys use on purpose
|
|
597
|
+
because their call site does a literal `.replace('{count}', …)` rather than
|
|
598
|
+
i18next interpolation. A translation that drops a placeholder renders a
|
|
599
|
+
sentence with a hole in it and no error, so this is checked mechanically
|
|
600
|
+
rather than by eye.
|
|
601
|
+
|
|
602
|
+
All three assertions were mutation-tested, including the single-brace form.
|
|
603
|
+
|
|
604
|
+
### A bug the test suite could not have caught
|
|
605
|
+
|
|
606
|
+
The first merge pass produced **duplicate keys** in four packs: the key list is
|
|
607
|
+
the union of what is missing across all eight, but the insert ran
|
|
608
|
+
unconditionally, so packs that already had `detail.created` / `detail.updated`
|
|
609
|
+
got a second copy. Every test still passed — at runtime the later property
|
|
610
|
+
simply wins, so the parity check saw a perfectly consistent object.
|
|
611
|
+
|
|
612
|
+
`tsc` caught it as TS1117 during `turbo build`. ESLint does not flag it, and a
|
|
613
|
+
runtime test _cannot_ — the duplicate is already collapsed before JS sees the
|
|
614
|
+
object. The compiler is the only possible guard here, and CI runs it. The merge
|
|
615
|
+
script now filters per pack against what that pack actually defines.
|
|
616
|
+
|
|
617
|
+
### Translation quality
|
|
618
|
+
|
|
619
|
+
Model-generated, and dense domain terminology (Gantt dependency types, the
|
|
620
|
+
import wizard's upsert/match-field vocabulary) is exactly where that is
|
|
621
|
+
weakest. This was raised before starting and the work was requested anyway, so
|
|
622
|
+
it ships as a **reviewable first draft, not a finished localization** — native
|
|
623
|
+
review is still worthwhile. What _is_ verified mechanically: key parity in both
|
|
624
|
+
directions, placeholder shape per string, and that no outbound agent message
|
|
625
|
+
was translated.
|
|
626
|
+
|
|
627
|
+
- 0502a7c: fix(i18n): the change card's Confirm button sent text the cloud gate does not accept
|
|
628
|
+
|
|
629
|
+
The English `console.ai.changesConfirmMessage` was
|
|
630
|
+
`"Confirm the changes — apply what you just proposed."`. The cloud confirm gate
|
|
631
|
+
(`service-ai-studio` `confirm-gate.ts` `APPROVAL_RE`) recognises
|
|
632
|
+
`apply (this|the) change` — **not** "apply what". So the message failed the
|
|
633
|
+
gate, and failing the gate is silent: the agent re-proposes instead of applying,
|
|
634
|
+
and the Confirm button on the change card simply looks inert.
|
|
635
|
+
|
|
636
|
+
This affected English conversations **and all eight locales that fall back to
|
|
637
|
+
English** for that key. It is now
|
|
638
|
+
`"Confirm — apply the change you just proposed."` — singular "the change", so it
|
|
639
|
+
still matches if the gate ever tightens to a word boundary. The Chinese string
|
|
640
|
+
was always fine (`确认修改` hits the 确认-anchored clause) and is unchanged.
|
|
641
|
+
|
|
642
|
+
The same literal lives in four places — the locale pack, the
|
|
643
|
+
`ChatbotEnhanced` prop default, its doc comment, and the `AiChatPage`
|
|
644
|
+
`defaultValue` — and all four are updated together.
|
|
645
|
+
|
|
646
|
+
**Why the existing guard missed it.** `i18n.test.ts` mirrored only the _Chinese_
|
|
647
|
+
clause of `APPROVAL_RE`; the English half was reduced to "starts with Confirm,
|
|
648
|
+
contains apply" because nothing in this repo could see the real pattern. That
|
|
649
|
+
weaker assertion passed against a string the gate rejected — the guard was
|
|
650
|
+
green and the feature was broken.
|
|
651
|
+
|
|
652
|
+
The mirror is now **verbatim, both clauses**, and drives an `it.each` over every
|
|
653
|
+
outbound approval message in both `zh` and `en`. Two supporting tests keep it
|
|
654
|
+
honest: one asserting the gate stays narrow (a plain build request like
|
|
655
|
+
"帮我搭建一个 CRM" must NOT read as approval), and one asserting
|
|
656
|
+
`planAnswerMessage` does _not_ match — it answers a structure question and must
|
|
657
|
+
never read as blanket approval.
|
|
658
|
+
|
|
659
|
+
The mirror is duplicated across a repo boundary by necessity (objectui cannot
|
|
660
|
+
import from cloud); the comment says so, so the next person changing
|
|
661
|
+
`APPROVAL_RE` knows to update it here too.
|
|
662
|
+
|
|
663
|
+
- c6fd752: fix(app-shell): localize the two `DeclaredActionsBar` strings that bypassed i18n (objectui#2762 P0-3)
|
|
664
|
+
|
|
665
|
+
The declared action _labels_ resolve through `useObjectLabel`, so a zh-CN
|
|
666
|
+
workspace got 通过 / 拒绝 buttons — sitting inside a toolbar whose accessible
|
|
667
|
+
name was the English literal `'Actions'`, above decision-output fields whose
|
|
668
|
+
help text read `Handed to the flow as a decision output.` Both strings are
|
|
669
|
+
authored by the bar itself rather than by metadata, and both skipped the locale
|
|
670
|
+
bundle entirely.
|
|
671
|
+
|
|
672
|
+
- `aria-label` now uses the existing `common.actions` key (a host-supplied
|
|
673
|
+
`label` still wins).
|
|
674
|
+
- The decision-output help text moves to new `actions.decisionOutput.help` /
|
|
675
|
+
`.helpMultiValue` keys, added across all ten shipped locales.
|
|
676
|
+
|
|
677
|
+
Worth being precise about why the help text needed fixing at all, since the
|
|
678
|
+
runtime _does_ localize action params: `useConsoleActionRuntime` runs every
|
|
679
|
+
param through `actionParamText`, but these params are synthesized here from the
|
|
680
|
+
record's `decision_output_defs`, so their key path (`outputs.<key>`) is dynamic
|
|
681
|
+
and no `_actions.<action>.params.*` bundle entry can ever match it. The
|
|
682
|
+
fallback is not a rare path — it is the only path, which is why the English
|
|
683
|
+
survived.
|
|
684
|
+
|
|
685
|
+
Not fixed, and deliberately: a decision output that arrives without a `label`
|
|
686
|
+
still renders a title-cased version of its machine key. That derived text
|
|
687
|
+
mirrors the framework's `humanizeFieldPath` convention, and the real fix is the
|
|
688
|
+
backend declaring the label — a client-side bundle cannot key off a dynamic
|
|
689
|
+
output name.
|
|
690
|
+
|
|
691
|
+
- e05f052: feat(i18n): translate the four highest-traffic namespaces into the eight trailing locales (objectui#2872 part a)
|
|
692
|
+
|
|
693
|
+
Backfills `console`, `home`, `topbar` and `layout` — 193 keys × 8 packs, 1,544
|
|
694
|
+
strings — so a ja/ko/de/fr/es/pt/ru/ar admin sees the AI console, the home
|
|
695
|
+
screen, the top bar and the system navigation in their own language instead of
|
|
696
|
+
silently falling back to English.
|
|
697
|
+
|
|
698
|
+
The gap in those eight packs drops from **469–471 keys to 277–279**. `en` and
|
|
699
|
+
`zh` remain at exact parity (2499 : 2499, zero difference in both directions).
|
|
700
|
+
|
|
701
|
+
This is the "high-frequency namespaces only" strategy from the objectui#2872
|
|
702
|
+
discussion, not a full backfill: `grid` (101), `gantt` (58), `dashboard` (25)
|
|
703
|
+
and the long tail stay on English fallback and remain tracked there.
|
|
704
|
+
|
|
705
|
+
**Four keys are deliberately left untranslated**, and that is the load-bearing
|
|
706
|
+
part of this change:
|
|
707
|
+
|
|
708
|
+
```
|
|
709
|
+
console.ai.planApproveMessage
|
|
710
|
+
console.ai.planApproveDefaultsMessage
|
|
711
|
+
console.ai.planAnswerMessage
|
|
712
|
+
console.ai.changesConfirmMessage
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
These are not labels. They are the text a button _transmits to the agent_, and
|
|
716
|
+
the cloud confirm gate (`service-ai-studio` `confirm-gate.ts` `APPROVAL_RE`)
|
|
717
|
+
decides whether that text reads as approval. It recognises Chinese and English
|
|
718
|
+
— nothing else. `AiChatPage` therefore selects them by the language of the
|
|
719
|
+
CONVERSATION rather than of the UI, and the `t()` call is _expected_ to miss in
|
|
720
|
+
every non-Chinese pack and fall through to its English `defaultValue`.
|
|
721
|
+
|
|
722
|
+
Translating them would be an outright regression: a German user's "Build it"
|
|
723
|
+
would start sending German, the gate would stop matching, and the agent would
|
|
724
|
+
re-propose instead of building — the button looks inert while nothing visibly
|
|
725
|
+
errors.
|
|
726
|
+
|
|
727
|
+
objectui#2900 shipped precisely that bug for `changesConfirmMessage`, which had
|
|
728
|
+
been added to all ten packs. **This change removes it from the eight**,
|
|
729
|
+
restoring the English fallback. A new guard,
|
|
730
|
+
`packages/i18n/src/__tests__/outbound-agent-messages.test.ts`, pins the
|
|
731
|
+
invariant in both directions: the four keys must be absent from the eight packs
|
|
732
|
+
AND present in `en`/`zh`, while every _other_ `console.ai` label must be
|
|
733
|
+
translated — so the narrow fix can't be over-applied into an excuse for leaving
|
|
734
|
+
surrounding labels in English.
|
|
735
|
+
|
|
736
|
+
Translations are model-generated and would benefit from native review; the
|
|
737
|
+
placeholder set of every string was verified programmatically against the
|
|
738
|
+
English source.
|
|
739
|
+
|
|
740
|
+
- 0502a7c: test(i18n): ratchet the four backfilled namespaces so they cannot silently erode
|
|
741
|
+
|
|
742
|
+
objectui#2903 translated `console`, `home`, `topbar` and `layout` into all ten
|
|
743
|
+
packs. Nothing stopped that from decaying: `fallbackLng: 'en'` means dropping a
|
|
744
|
+
key from `de` renders English, which reads as "not translated yet" rather than
|
|
745
|
+
"we lost this", and the missing-key handler is dev-only so CI never sees it.
|
|
746
|
+
|
|
747
|
+
This is objectui#2872's P3 (full parity test) applied **only to the namespaces
|
|
748
|
+
that are actually complete**. Full parity would fail today by ~277 keys per
|
|
749
|
+
pack with no action attached to it, which is a broken build rather than a
|
|
750
|
+
guard. Widen `RATCHETED_NAMESPACES` as each remaining namespace is translated —
|
|
751
|
+
not before.
|
|
752
|
+
|
|
753
|
+
Asserts both directions, because the packs have drifted both ways before:
|
|
754
|
+
|
|
755
|
+
- every ratcheted `en` key exists in all nine other packs;
|
|
756
|
+
- no pack defines a ratcheted key that `en` lacks — objectui#2872 part (b) was
|
|
757
|
+
exactly this failure, 74 keys deep, hidden behind a component-private
|
|
758
|
+
fallback so English "happened to" render.
|
|
759
|
+
|
|
760
|
+
The four outbound agent messages are excluded, since they are deliberately
|
|
761
|
+
absent from the eight non-gate packs; `outbound-agent-messages.test.ts` owns
|
|
762
|
+
that invariant and the two guards would otherwise contradict each other.
|
|
763
|
+
|
|
764
|
+
A non-vacuity assertion pins the ratchet at >300 keys and requires every named
|
|
765
|
+
namespace to contribute, so a rename can't quietly reduce the whole file to a
|
|
766
|
+
no-op.
|
|
767
|
+
|
|
768
|
+
- faad45e: fix(fields): render `image` fields consistently and add click-to-zoom (#2836)
|
|
769
|
+
|
|
770
|
+
An `image` field rendered differently — and wrongly — on three surfaces:
|
|
771
|
+
|
|
772
|
+
- **Edit form showed broken thumbnails.** A record read back its `image` value
|
|
773
|
+
as a bare `sys_file` id (the reference form), but `readFileValue` returned an
|
|
774
|
+
id with no URL — the comment assumed the read path expands it, which the
|
|
775
|
+
edit-form data path does not. The result was `<img src="">`. `file-value` now
|
|
776
|
+
derives the stable download URL (`/api/v1/storage/files/:id`, which
|
|
777
|
+
302-redirects to a signed URL and works directly as `<img src>`) for a bare
|
|
778
|
+
id or an id-only object, so every widget and cell renderer resolves one.
|
|
779
|
+
- **Inline edit leaked the raw storage URL.** `InlineFieldInput` had no branch
|
|
780
|
+
for file-backed types and fell through to a plain text input showing
|
|
781
|
+
`/api/v1/storage/files/…`. It now renders the same upload widgets the form
|
|
782
|
+
uses (`image`/`avatar`/`signature`/`file`/`video`/`audio`).
|
|
783
|
+
- **Hard-coded English.** `ImageField`'s upload/crop/remove/alt strings now go
|
|
784
|
+
through `t('fields.image.*')` (en + zh added).
|
|
785
|
+
|
|
786
|
+
Also adds an `ImageLightbox` — click a read-only thumbnail (detail or list cell)
|
|
787
|
+
to open a full-screen preview; multiple images get prev/next navigation, a
|
|
788
|
+
position counter and arrow-key support, a single image just the image. In a
|
|
789
|
+
grid cell the click is `stopPropagation`-guarded so enlarging doesn't also open
|
|
790
|
+
the row.
|
|
791
|
+
|
|
792
|
+
- 09c6a17: fix(grid): localize import result errors (objectstack#3566)
|
|
793
|
+
|
|
794
|
+
The import completion screen rendered the raw English server message verbatim —
|
|
795
|
+
e.g. `Row 6 (position): position: "装配工" matches more than one
|
|
796
|
+
os_tianshun_ehr_position — use a unique value or the record id` — with the field
|
|
797
|
+
name twice, an internal object api-name, all in English, while the dry-run panel
|
|
798
|
+
already localized the same errors.
|
|
799
|
+
|
|
800
|
+
- The result list now runs through the same `formatDryRunError` path (driving
|
|
801
|
+
off the structured error `code`, resolving the api-name to its field label,
|
|
802
|
+
dropping the duplicated `<api-name>:` prefix). Threaded the error `code`
|
|
803
|
+
through `ImportResult.errors` to make this possible.
|
|
804
|
+
- Added code-driven translations for the remaining structured import errors —
|
|
805
|
+
`invalid_boolean` / `invalid_number` / `invalid_date` / `invalid_option` /
|
|
806
|
+
`required` / `AMBIGUOUS_MATCH` — with Chinese (`zh`) copy in `@object-ui/i18n`
|
|
807
|
+
alongside the existing reference errors.
|
|
808
|
+
|
|
809
|
+
- ba73a02: fix(kanban): surface off-column records in an "Uncategorized" lane instead of dropping them (#2792)
|
|
810
|
+
|
|
811
|
+
Records whose `groupBy` value matched no declared column were bucketed and then silently discarded — the board rendered empty while the list footer still counted the rows, so it read as data loss (a status the board doesn't render, an edited/removed picklist option, imported legacy data, or an empty value all triggered it). They now land in a trailing "Uncategorized" lane so no record is invisible and the visible card total reconciles with the record count. Dragging a card out of that lane into a real column repairs its status; the drag handler refuses to persist a move _into_ the lane (its sentinel id is not a real option). Adds `kanban.uncategorized` to the en/zh bundles.
|
|
812
|
+
|
|
813
|
+
- 9b4b952: fix(i18n): make `en` the complete source of truth for grid import and set-password (objectui#2872 b/c)
|
|
814
|
+
|
|
815
|
+
The `en` and `zh` packs had drifted in both directions, silently, because
|
|
816
|
+
`fallbackLng: 'en'` degrades a missing key into English rather than an error and
|
|
817
|
+
the missing-key handler only fires in dev.
|
|
818
|
+
|
|
819
|
+
- **74 keys existed only in `zh`.** `grid.import.*` and `auth.setPassword.*` had
|
|
820
|
+
never been added to `en`, so no other locale could translate them: the English
|
|
821
|
+
text came from call-site `defaultValue:` args and a private map inside
|
|
822
|
+
`ImportWizard`. They now live in `en`, which is what translators and
|
|
823
|
+
`os i18n extract` read.
|
|
824
|
+
- **4 `en` keys were missing from `zh`** (`console.commandPalette.title`,
|
|
825
|
+
two `console.ai.suggestions.metadataAssistant.*`, `help.keyboardShortcuts`),
|
|
826
|
+
so Chinese users saw English.
|
|
827
|
+
|
|
828
|
+
`grid.import` in particular had three disagreeing sources — the `en` pack (62
|
|
829
|
+
keys), `zh` (130) and `ImportWizard`'s own fallback map (133), union 134, no two
|
|
830
|
+
the same set. All three are now aligned on 134.
|
|
831
|
+
|
|
832
|
+
The wizard's fallback map is kept, not deleted: it is what lets the wizard render
|
|
833
|
+
with no `I18nProvider` mounted (standalone embedding, unit tests). It is instead
|
|
834
|
+
pinned to the `en` pack by a new test, so the two can no longer drift.
|
|
835
|
+
|
|
836
|
+
`SetPasswordPage` drops its now-redundant inline `defaultValue:` args; the text
|
|
837
|
+
is byte-identical, it just comes from the pack now.
|
|
838
|
+
|
|
839
|
+
Adds two guards, both mutation-verified:
|
|
840
|
+
- `en` ↔ `zh` full key parity, asserted in both directions. The other eight
|
|
841
|
+
packs are still ~357 keys behind and are tracked separately (objectui#2872
|
|
842
|
+
part a), so they are deliberately not asserted yet.
|
|
843
|
+
- `IMPORT_DEFAULT_TRANSLATIONS` ↔ `en.grid.import`, same keys and same text.
|
|
844
|
+
|
|
845
|
+
- 7d46648: fix(hooks): stop calling translation hooks inside try/catch (objectui#2879)
|
|
846
|
+
|
|
847
|
+
Eleven call sites wrapped a React hook in `try`/`catch` to make it
|
|
848
|
+
"provider-safe". `useObjectTranslation` and `useObjectLabel` already are — they
|
|
849
|
+
read context optionally and fall back to react-i18next's global instance, and
|
|
850
|
+
never throw. The `catch` bought nothing and cost correctness: a throw _after_
|
|
851
|
+
the hook ran desyncs hook order on the next render, because React matches hooks
|
|
852
|
+
positionally. objectui#2595/#2596 fixed exactly this in `@object-ui/i18n`'s
|
|
853
|
+
`createSafeTranslation`; nine plugin-local re-implementations kept their own
|
|
854
|
+
copy of the bug, and two more (`ObjectTimeline`, `ObjectView`) were found by the
|
|
855
|
+
new lint rule below — `ObjectView` had even suppressed
|
|
856
|
+
`react-hooks/rules-of-hooks` inline to keep it.
|
|
857
|
+
|
|
858
|
+
- Six exact re-implementations now delegate to `createSafeTranslation`:
|
|
859
|
+
`plugin-detail`, `plugin-timeline`, `plugin-list`, `plugin-calendar`,
|
|
860
|
+
`plugin-grid`'s `ObjectGrid`, `plugin-designer`.
|
|
861
|
+
- `components`' `data-table` also delegates; `createSafeTranslation` now
|
|
862
|
+
returns `language` alongside `t` so consumers that localize dates don't need
|
|
863
|
+
a second hook call. Purely additive.
|
|
864
|
+
- `plugin-gantt` and `plugin-grid`'s `ImportWizard` keep their local hooks —
|
|
865
|
+
they fall back _per key_, which a single-probe factory cannot express and
|
|
866
|
+
which their comments justify (a host dictionary that covers common keys but
|
|
867
|
+
lags on newer ones). Only the `try`/`catch` is removed.
|
|
868
|
+
- `ObjectTimeline` and `ObjectView` call the hook directly and probe the
|
|
869
|
+
returned value, mirroring `useSafeFieldLabel`.
|
|
870
|
+
|
|
871
|
+
Adds `object-ui/no-try-catch-around-hook` (error) so a twelfth copy fails CI.
|
|
872
|
+
It only matches `use*` names, accepts member calls solely on `React` (so
|
|
873
|
+
`vi.useRealTimers()` is not a hook), and resets its try-depth inside nested
|
|
874
|
+
functions (so `renderHook(() => useThing())` inside a `try` is fine) — both
|
|
875
|
+
false positives were real code in this repo and are pinned in the rule's tests.
|
|
876
|
+
|
|
877
|
+
`eslint-rules/**/*.test.js` matched no vitest project glob, so the local
|
|
878
|
+
plugin's specs had never run in CI. They are now included; all three pass.
|
|
879
|
+
|
|
880
|
+
`ObjectTimeline`'s test mock of `@object-ui/react` omitted `useObjectLabel` —
|
|
881
|
+
the removed `try`/`catch` had been silently absorbing that gap. The mock is now
|
|
882
|
+
complete.
|
|
883
|
+
|
|
884
|
+
- bb4aa25: fix(i18n): apply globalActions label overlays to actions surfaced on a record-detail action bar (objectui#3372)
|
|
885
|
+
|
|
886
|
+
On a record-detail action bar the caller passes `objectDef.name` for **every**
|
|
887
|
+
action, so a `globalAction` surfaced there (e.g. `log_call`) looked up
|
|
888
|
+
`objects.<obj>._actions.<action>.label`, missed, and leaked the English
|
|
889
|
+
metadata literal ("Log a Call") instead of its `globalActions.<action>.label`
|
|
890
|
+
overlay ("记录通话"). Object-owned actions on the same bar translated fine,
|
|
891
|
+
which is what made the gap visible.
|
|
892
|
+
|
|
893
|
+
`useObjectLabel()`'s action resolvers now mirror the canonical
|
|
894
|
+
`@objectstack/spec` resolver (`system/i18n-resolver.lookupActionField`): when an
|
|
895
|
+
action is object-scoped, the object key still wins, but `globalActions.<action>.*`
|
|
896
|
+
is consulted as a fallback before returning the literal. This applies uniformly
|
|
897
|
+
to `actionLabel`, `actionConfirm`, `actionSuccess`, `actionDescription`,
|
|
898
|
+
`actionResultDialog`, `actionParamText`, and `actionParamOptionLabel`, so a
|
|
899
|
+
globalAction resolves the same on a record-detail action bar as it does
|
|
900
|
+
everywhere else. App-namespace discovery also recognises a `globalActions`-only
|
|
901
|
+
bundle (one with no object/field entries).
|
|
902
|
+
|
|
903
|
+
- 75f1cdf: fix(auth): localize the ADR-0069 remediation gate and the auth split-panel (#2870)
|
|
904
|
+
|
|
905
|
+
`RemediationOverlay` had no i18n at all. It is the full-screen gate mounted
|
|
906
|
+
unconditionally at `ConsoleShell` (`fixed inset-0 z-[200]`) that a user hits
|
|
907
|
+
when the backend returns `PASSWORD_EXPIRED` or `MFA_REQUIRED` — there is no
|
|
908
|
+
route around it, so a user who could not read English could not get back into
|
|
909
|
+
the product. That makes it a usability block rather than a cosmetic gap.
|
|
910
|
+
|
|
911
|
+
- New `auth.remediation.*` namespace in all ten locale packs, covering both
|
|
912
|
+
branches of the gate: expired-password (title, three field labels, submit /
|
|
913
|
+
submitting, mismatch and failure messages) and MFA enrolment (password step,
|
|
914
|
+
QR scan copy, backup-code disclosure, code entry, verify / verifying, and the
|
|
915
|
+
enrolment and invalid-code failures), plus the shared "sign out instead" exit.
|
|
916
|
+
- Validation and failure messages are translated where they are raised, since
|
|
917
|
+
they are held in component state and rendered later.
|
|
918
|
+
- The server-provided `remediationRequired.message` is left untouched; only the
|
|
919
|
+
empty-message fallback is localized.
|
|
920
|
+
- `AuthPageLayout`'s two marketing strings move to `auth.layout.*`. The forms it
|
|
921
|
+
wraps were already localized, so the split-panel had been rendering half in
|
|
922
|
+
the user's language and half in English.
|
|
923
|
+
|
|
924
|
+
Adds a locale-parity test over both namespaces, asserting an identical key set
|
|
925
|
+
across all ten packs, a non-empty string at every leaf, and that prose differs
|
|
926
|
+
from English (short labels like "Continue" legitimately collide). i18next falls
|
|
927
|
+
back to `en` silently and its missing-key handler is dev-only, so a key added to
|
|
928
|
+
one pack and forgotten elsewhere is invisible in whichever locales get tested by
|
|
929
|
+
hand.
|
|
930
|
+
|
|
931
|
+
- 53642d4: fix(core,fields): a string `$orderby` is a clause, not a character array — and localize the sharing-rule widgets (objectstack#3821)
|
|
932
|
+
|
|
933
|
+
**The recipient picker listed nothing, ever.** `QueryParams['$orderby']` was
|
|
934
|
+
typed as `Record | string[] | SortObject[]`, so `queryParamsToRecord` sent any
|
|
935
|
+
non-array value through `Object.entries`. Handed the clause string `'name asc'`
|
|
936
|
+
— which callers do build by hand — it walked the string index by index and
|
|
937
|
+
emitted `$orderby=0 n,1 a,2 m,3 e,4 ,5 a,6 s,7 c`. The server sorted by columns
|
|
938
|
+
that don't exist and every row was filtered out, so
|
|
939
|
+
`sys_sharing_rule.recipient_id` rendered "No matches" for every recipient type
|
|
940
|
+
and no sharing rule could be created from the Console. `ObjectGrid` builds the
|
|
941
|
+
same shape from a schema-level `sort` in three places, so grids with a string
|
|
942
|
+
sort silently showed an empty table.
|
|
943
|
+
|
|
944
|
+
A string `$orderby` is now passed through verbatim (the server's OData
|
|
945
|
+
normalizer has always parsed `'name asc'`), and the type admits `string`.
|
|
946
|
+
`RecipientPickerField` additionally switched to the structured
|
|
947
|
+
`{ name: 'asc' }` form so it can't regress this way against any data source.
|
|
948
|
+
|
|
949
|
+
**The three sharing-rule authoring widgets never had translations.**
|
|
950
|
+
`ObjectRefField`, `RecipientPickerField` and `FilterConditionField` hardcoded
|
|
951
|
+
their English copy — a Chinese Console showed "Select an object", "Select a
|
|
952
|
+
user", "Search…", "No matches", "Edit as JSON". They now go through
|
|
953
|
+
`useFieldTranslation` like every other widget, with keys added under `fields.*`
|
|
954
|
+
in all ten locales.
|
|
955
|
+
|
|
956
|
+
The recipient placeholder was the interesting one: it read
|
|
957
|
+
`` `Select a ${recipientType.replace(/_/g,' ')}` ``, interpolating the enum
|
|
958
|
+
value into an English sentence — a shape no locale can translate. It is now a
|
|
959
|
+
per-type key (`fields.recipient.selectUser`, `…selectBusinessUnit`, …), so
|
|
960
|
+
"选择业务单元" and "Select a business unit" no longer have to share a structure.
|
|
961
|
+
|
|
962
|
+
**Editing a rule silently dropped its recipient.** The picker resets the stored
|
|
963
|
+
id when `recipient_type` changes, because an id valid for a user is meaningless
|
|
964
|
+
for a team. It treated the edit form's `'' → 'user'` hydration as such a change:
|
|
965
|
+
opening any saved rule blanked the recipient, and saving persisted the blank.
|
|
966
|
+
Only a non-empty predecessor now counts as a type switch.
|
|
967
|
+
|
|
968
|
+
**Building a filter submitted the surrounding form.** None of `FilterBuilder`'s
|
|
969
|
+
controls declared `type="button"`, and a bare `<button>` inside a `<form>`
|
|
970
|
+
defaults to `type="submit"`. Adding, removing or clearing a condition therefore
|
|
971
|
+
submitted the sharing-rule dialog — firing validation mid-edit, and on an
|
|
972
|
+
already-valid form saving the record before the admin was done.
|
|
973
|
+
|
|
974
|
+
**A rejected write showed the user raw server diagnostics.** The form rendered
|
|
975
|
+
`error.message` verbatim, so a sharing / RLS denial reached the dialog and the
|
|
976
|
+
toast as `FORBIDDEN: insufficient privileges to update showcase_private_note
|
|
977
|
+
pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and the
|
|
978
|
+
record id to whoever hit it. Permission failures now render localized copy
|
|
979
|
+
(`form.noPermissionToSave`, added in all ten locales), with the server text kept
|
|
980
|
+
on the console for debugging; other failures still show the server's message,
|
|
981
|
+
which is the useful part, and fall back to `form.submitFailed` when there is
|
|
982
|
+
none — replacing the previously hardcoded English "An error occurred during
|
|
983
|
+
submission".
|
|
984
|
+
|
|
985
|
+
**The detail header offered "Edit" on records the user may only read.** Object
|
|
986
|
+
permissions can't express "this one record is read-only" — a read-only sharing
|
|
987
|
+
grant sits inside an object the user may otherwise edit — so the header showed
|
|
988
|
+
the primary Edit CTA, opened the form, and let the user retype a field before
|
|
989
|
+
the server rejected the save. `DetailView` now gates Edit / Delete on the
|
|
990
|
+
object-level check AND on the explain engine's record-grained verdict
|
|
991
|
+
(`POST /api/v1/security/explain` with a `recordId`, ADR-0090 D6 / ADR-0095 C2 —
|
|
992
|
+
the same pipeline the enforcement middleware runs, so button and server cannot
|
|
993
|
+
disagree). Explaining oneself needs no special permission. The probe is one
|
|
994
|
+
cached request per record, skipped entirely when the object-level check already
|
|
995
|
+
says no, and **fails open** on every uncertainty — an unanswered hint must never
|
|
996
|
+
be the reason a permitted user cannot act; the server stays the authority
|
|
997
|
+
(ADR-0057 D10).
|
|
998
|
+
|
|
999
|
+
**A long option rendered straight past the combobox border.** `Combobox`'s
|
|
1000
|
+
trigger pinned itself to the component's `w-[200px]` default while the fields
|
|
1001
|
+
around it ran the full form column, and the selected label was a bare text child
|
|
1002
|
+
of a flex button — flex items need `truncate` AND `min-w-0` to clip, and it had
|
|
1003
|
+
neither. So "成员 (showcase_project_membership)" in the object picker overflowed
|
|
1004
|
+
the control and collided with the field beside it. The label now truncates, the
|
|
1005
|
+
trigger can shrink, the dropdown matches the trigger's width instead of a
|
|
1006
|
+
hardcoded 200px (a widened combobox used to clip its own options), and the two
|
|
1007
|
+
sharing-rule pickers ask for `w-full` so they line up with every other input.
|
|
1008
|
+
|
|
1009
|
+
Hardens `evaluatePermission` while there: a role config carrying only
|
|
1010
|
+
`fieldPermissions` (no `actions`) made `check()` throw a TypeError that
|
|
1011
|
+
propagated out of the render. A permission check must not be able to crash a
|
|
1012
|
+
view.
|
|
1013
|
+
|
|
1014
|
+
Browser-verified against the framework showcase Console in Chinese: object /
|
|
1015
|
+
criteria / recipient copy is fully localized, the recipient dropdown lists real
|
|
1016
|
+
users, business units and positions, a saved rule reopens with its recipient and
|
|
1017
|
+
criteria intact, editing the filter no longer submits, and a rule created
|
|
1018
|
+
end-to-end stores a real record id rather than free text. The criteria authored
|
|
1019
|
+
in the builder is honored by the evaluator: `{"pinned":true}` on an owner-private
|
|
1020
|
+
object granted the recipient exactly the matching records and nothing else.
|
|
1021
|
+
|
|
1022
|
+
- c6aaed8: fix(i18n): retire four hand-rolled zh/en branches (objectui#2871, part 1)
|
|
1023
|
+
|
|
1024
|
+
Four surfaces decided their language with a hand-written `startsWith('zh')`
|
|
1025
|
+
check instead of the locale packs, so the other eight shipped languages
|
|
1026
|
+
silently rendered English and the strings could never be translated without a
|
|
1027
|
+
code change.
|
|
1028
|
+
|
|
1029
|
+
- **`RecordTitleChip`** carried a private zh-CN/zh-TW dictionary behind a
|
|
1030
|
+
comment claiming "components is i18n-free". That is not true —
|
|
1031
|
+
`@object-ui/components` declares `@object-ui/i18n` and its sibling
|
|
1032
|
+
`containers.tsx` already uses it. All four of its keys (`detail.copied`,
|
|
1033
|
+
`detail.copyRecordId`, `detail.addToFavorites`, `detail.removeFromFavorites`)
|
|
1034
|
+
already existed in **all ten packs**, so this deletes ~35 lines and fixes ten
|
|
1035
|
+
locales with zero new translations. It renders on every record detail page.
|
|
1036
|
+
- **`EnvironmentListToolbar`**'s three state-aware CTA labels move to a new
|
|
1037
|
+
`environment.*` namespace. This surface had already regressed once for the
|
|
1038
|
+
same reason (#844) and was fixed then with inline `{en,zh}` pairs.
|
|
1039
|
+
- **`StudioAiCopilot`**'s dock title moves to the Studio catalog as
|
|
1040
|
+
`engine.studio.aiCopilot`.
|
|
1041
|
+
- **`StudioHomePage.relativeTime`** now uses `Intl.RelativeTimeFormat` with
|
|
1042
|
+
`numeric: 'auto'` instead of five `zh ? … : …` ternaries. This is strictly
|
|
1043
|
+
better than adding ten catalog keys: it covers every locale, applies the
|
|
1044
|
+
correct plural rules, and yields "yesterday" / 「昨天」 rather than "1d ago".
|
|
1045
|
+
Arabic gets its dual form («أسبوعين») — something a ternary cannot express.
|
|
1046
|
+
|
|
1047
|
+
The new `environment.*` keys are added to all ten packs, so this does not widen
|
|
1048
|
+
the gap tracked by objectui#2872 part (a).
|
|
1049
|
+
|
|
1050
|
+
`EnvironmentListToolbar`'s tests now render inside a real `I18nProvider` pinned
|
|
1051
|
+
to `en`. Without one, `t()` returns the raw key, so the previous assertions on
|
|
1052
|
+
literal English would have been asserting nothing.
|
|
1053
|
+
|
|
1054
|
+
- 263f885: fix(i18n): delete the four `pick({en,zh})` clones (objectui#2871, part 2)
|
|
1055
|
+
|
|
1056
|
+
Four files each carried an identical private resolver:
|
|
1057
|
+
|
|
1058
|
+
```ts
|
|
1059
|
+
function pick(label: I18n): string {
|
|
1060
|
+
const lang = document.documentElement.getAttribute("lang") || "en";
|
|
1061
|
+
return lang.toLowerCase().startsWith("zh") ? label.zh : label.en;
|
|
1062
|
+
}
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
Only Chinese was ever handled, so ja/ko/de/fr/es/pt/ru/ar silently rendered
|
|
1066
|
+
English — and because the copy was baked into the components as inline
|
|
1067
|
+
`{en, zh}` pairs, no translator could reach it. All four copies are deleted
|
|
1068
|
+
along with their `I18n` type alias.
|
|
1069
|
+
|
|
1070
|
+
Migrated to the locale packs, **all ten languages**:
|
|
1071
|
+
|
|
1072
|
+
- `excelImport.*` (8 keys) — `ExcelImportBar`. The completion toast becomes a
|
|
1073
|
+
proper `{{count}}` / `{{object}}` interpolation instead of a template literal
|
|
1074
|
+
baked into both language variants.
|
|
1075
|
+
- `cloudOnboarding.*` (5 keys) — `CloudOnboardingNext`, the Cloud welcome page.
|
|
1076
|
+
- `aiModelStatus.*` (11 keys) — `CloudAiModelStatus`, including the
|
|
1077
|
+
`sourceLabel()` enum→prose helper (now `t`-driven with a `{{source}}`
|
|
1078
|
+
placeholder) and the three `ModelRow` labels. The conditional
|
|
1079
|
+
`(HTTP nnn)` fragment becomes two whole sentences rather than a string
|
|
1080
|
+
spliced mid-clause, which is not translatable into every word order.
|
|
1081
|
+
- `chatbotQuota.*` (4 keys) — the AI quota banner in `ChatbotEnhanced`.
|
|
1082
|
+
|
|
1083
|
+
The chatbot banner keeps choosing between the server's `quota.message` (zh) and
|
|
1084
|
+
`quota.messageEn` — that pair is server-owned — but now decides using the
|
|
1085
|
+
console's active language instead of `navigator.language`, which had ignored
|
|
1086
|
+
the in-app locale switcher entirely.
|
|
1087
|
+
|
|
1088
|
+
`CloudOnboardingNext`'s tests now render inside a real `I18nProvider`; without
|
|
1089
|
+
one `t()` returns the raw key, so the previous assertions on literal English
|
|
1090
|
+
were asserting nothing.
|
|
1091
|
+
|
|
1092
|
+
This completes the `pick()` cluster from #2871. The remaining
|
|
1093
|
+
`startsWith('zh')` sites are the ones that classification marked KEEP —
|
|
1094
|
+
`LoadingScreen` (bootstrap, selects real locale packs before i18next is up),
|
|
1095
|
+
`conversationLanguage` (detects the chat's language for the agent, not UI
|
|
1096
|
+
copy), `containers.tsx` (normalises author-supplied schema data; its `'与'`
|
|
1097
|
+
separator is a CJK typography rule), and the Studio catalog / `field-types.ts`
|
|
1098
|
+
data catalog.
|
|
1099
|
+
|
|
1100
|
+
- dc334da: fix(i18n): close the last three zh-branch gaps (objectui#2871, part 3)
|
|
1101
|
+
|
|
1102
|
+
The three items the #2871 classification marked as real but _not_ a
|
|
1103
|
+
migrate-the-copy fix. Each needed a different remedy.
|
|
1104
|
+
|
|
1105
|
+
**`LoadingScreen` — ten languages collapsed to two.** The boot splash already
|
|
1106
|
+
selected real locale packs (not inline copy), but through
|
|
1107
|
+
`lang.startsWith('zh') ? zh : en`, so a ja/ko/de user watched the whole startup
|
|
1108
|
+
in English. It now indexes `builtInLocales` by the two-letter prefix.
|
|
1109
|
+
|
|
1110
|
+
Each field falls back to `en` **individually**, which matters: `console.*` is
|
|
1111
|
+
one of the namespaces that trails in the non-`zh` packs (objectui#2872 part a),
|
|
1112
|
+
so a whole-object swap would have rendered `undefined` on the splash rather
|
|
1113
|
+
than English. `console.loadingHint` was in fact missing from all eight — added
|
|
1114
|
+
here, since a blank line under the progress list is worse than an English one.
|
|
1115
|
+
|
|
1116
|
+
**`containers.tsx` — two language sources that could disagree.** The tab-label
|
|
1117
|
+
call sites resolved `language` from `useObjectTranslation()`, then handed the
|
|
1118
|
+
string to `translateLabel`, which called `detectLocale()` and read
|
|
1119
|
+
`document.documentElement.lang` on its own. Those update independently, so an
|
|
1120
|
+
in-app language switch could leave a tab label and its surrounding chrome in
|
|
1121
|
+
different languages until the next reload. `language` is now threaded in, and
|
|
1122
|
+
`detectLocale` is deleted so nothing reaches for the DOM again.
|
|
1123
|
+
|
|
1124
|
+
**`field-types.ts` — a two-language data catalog.** `FieldTypeMeta` carried a
|
|
1125
|
+
`labelZh` column beside `label`, which capped the field-type picker at English
|
|
1126
|
+
or Chinese by construction. The 46 type names and 9 category names move into
|
|
1127
|
+
the Studio catalog as `engine.fieldType.<id>` / `engine.fieldCategory.<cat>`,
|
|
1128
|
+
generated from the existing values so no wording changes. This removes the
|
|
1129
|
+
`isZh` helper from **both** `ObjectFieldInspector` and `ObjectFormCanvas` — the
|
|
1130
|
+
two files the classification listed as "keep the component, fix the catalog".
|
|
1131
|
+
|
|
1132
|
+
The picker's search filter previously matched `id`, the English label, and
|
|
1133
|
+
`labelZh` — so searching in Japanese or German matched nothing. It now matches
|
|
1134
|
+
the label as the user actually sees it.
|
|
1135
|
+
|
|
1136
|
+
## 16.1.0
|
|
1137
|
+
|
|
1138
|
+
### Minor Changes
|
|
1139
|
+
|
|
1140
|
+
- af1b0db: feat(i18n): localize action result dialogs via the `_actions.<action>.resultDialog` convention
|
|
1141
|
+
|
|
1142
|
+
The post-success secret-reveal dialog (create-user temporary password, 2FA
|
|
1143
|
+
backup codes, OAuth client secrets) always rendered the hardcoded English
|
|
1144
|
+
metadata literals — the spec bundles now carry `resultDialog` translations
|
|
1145
|
+
(objectstack `_actions.<action>.resultDialog.*`), but nothing resolved them
|
|
1146
|
+
client-side.
|
|
1147
|
+
|
|
1148
|
+
- **@object-ui/i18n.** `useObjectLabel()` gains `actionResultDialog(objectName,
|
|
1149
|
+
actionName, spec)`: overlays translated `title` / `description` /
|
|
1150
|
+
`acknowledge` and per-field labels onto the metadata spec, falling back to
|
|
1151
|
+
the literals. The `fields` node is keyed by the LITERAL result-field path
|
|
1152
|
+
(may contain dots, e.g. `"user.email"`), so it is fetched whole with
|
|
1153
|
+
`returnObjects` and indexed directly — never resolved through a dotted
|
|
1154
|
+
i18next key. Built-in locale packs also translate the dialog's fallback
|
|
1155
|
+
`defaultTitle` / `acknowledge` (previously English in all ten locales) and
|
|
1156
|
+
add the new `actions.resultDialog.copyAll` key.
|
|
1157
|
+
- **@object-ui/app-shell.** The result-dialog handlers in
|
|
1158
|
+
`useConsoleActionRuntime` and `RecordDetailView` accept the action context
|
|
1159
|
+
(already passed by `ActionRunner`) and localize the spec before opening the
|
|
1160
|
+
dialog; `ActionResultDialog`'s hardcoded "Copy all" button now goes through
|
|
1161
|
+
`actions.resultDialog.copyAll`.
|
|
1162
|
+
|
|
1163
|
+
### Patch Changes
|
|
1164
|
+
|
|
1165
|
+
- 0318118: fix(app-shell): block ActionParamDialog submit while a file/image param is uploading; map spec `autonumber` (ADR-0059 follow-ups)
|
|
1166
|
+
|
|
1167
|
+
Two follow-ups to the shared-field-widget param rendering (ADR-0059):
|
|
1168
|
+
|
|
1169
|
+
- **Upload-in-progress guard.** A `file`/`image` param's value only becomes its
|
|
1170
|
+
fileId once the presigned upload settles, so confirming mid-upload sent an
|
|
1171
|
+
empty/stale value. `FileField`/`ImageField` now surface their upload state via
|
|
1172
|
+
an optional `onUploadingChange` prop (shared `useUploadingSignal` hook,
|
|
1173
|
+
ignored by other widgets); `ActionParamDialog` wires it for `file`/`image`
|
|
1174
|
+
params and disables Confirm (label → "Uploading…", new `actionDialog.uploading`
|
|
1175
|
+
i18n key across all locales) plus blocks submit while any upload is in flight.
|
|
1176
|
+
- **`autonumber` spelling.** `mapFieldTypeToFormType` now maps the spec
|
|
1177
|
+
`FieldType` spelling `autonumber` (in addition to the widget-map key
|
|
1178
|
+
`auto_number`) to the AutoNumber widget, so a spec-typed `autonumber`
|
|
1179
|
+
field/param no longer falls through to the plain text input — fixes the object
|
|
1180
|
+
form path as well as action params.
|
|
1181
|
+
|
|
1182
|
+
- 1100a8b: feat(plugin-gantt)!: remove the 移动端二维码 (mobile QR share) context-menu item
|
|
1183
|
+
|
|
1184
|
+
The QR-share feature is removed outright: the context-menu item, the QR dialog,
|
|
1185
|
+
the `taskUrl` prop on `GanttView`, the URL wiring in `ObjectGantt`, the
|
|
1186
|
+
`gantt.menu.qrcode` / `gantt.qr.*` i18n keys (en/zh) and the `qrcode`
|
|
1187
|
+
dependency are all deleted. It baked one consumer's app-specific requirement
|
|
1188
|
+
(scan-to-open on mobile) into the generic gantt renderer, and what it encoded —
|
|
1189
|
+
the desktop console record URL — was not even the right target for that
|
|
1190
|
+
requirement. Apps that need scan-to-mobile flows should implement them
|
|
1191
|
+
app-side against their own mobile surface.
|
|
1192
|
+
|
|
1193
|
+
- 7abe4cd: **Console user-import wizard defaults to the `auto` password policy (tracks framework#3236).** The "Sign-in setup for imported users" selector gains an **Automatic (recommended)** option and it is now the default (was "No password"). `auto` decides per row on the server: reachable users get an invitation (email / SMS), anyone who can't be reached gets a one-time password shown once on the result screen — so it works with or without an email/SMS service, and the one-time-password reveal now surfaces only the rows that actually fell back (instead of the whole batch under `temporary`).
|
|
1194
|
+
|
|
1195
|
+
The other three policies are unchanged and still selectable: `invite` (force invitations, unreachable rows fail), `temporary` (force one-time passwords for every row), `none` (identity only). New `console.identityImport.policy.auto` / `policyHint.auto` strings added for `en` and `zh`; the `none` label drops its "(recommended)" marker.
|
|
1196
|
+
|
|
1197
|
+
- ebe6494: chore(lint): clear the baseline lint errors in nine more packages (objectui#2713 Wave 2)
|
|
1198
|
+
|
|
1199
|
+
Second wave of the #2713 lint-gate restoration (after #2730). These nine package
|
|
1200
|
+
lints were red at baseline on `main`, so their per-package `lint` gate could not
|
|
1201
|
+
catch new violations. Cleared every **error** (no behavior change; warnings out
|
|
1202
|
+
of scope):
|
|
1203
|
+
|
|
1204
|
+
- **`react-hooks/rules-of-hooks`** (`i18n`, `plugin-grid`, `plugin-view`,
|
|
1205
|
+
`plugin-list`) — translation helpers (`useSafeFieldLabel`,
|
|
1206
|
+
`useRowActionTranslation`, `useViewLabel`, `useViewTabLabel`, `useMoreLabel`)
|
|
1207
|
+
wrapped a provider-safe hook (`useObjectTranslation`/`useObjectLabel`, which
|
|
1208
|
+
never throw) in try/catch; removed the wrapper (the same fix #2709 applied in
|
|
1209
|
+
fields). `plugin-kanban` `ObjectKanban` moved its `if (error)` early return
|
|
1210
|
+
below the `useCallback` so hooks run unconditionally. `collaboration`
|
|
1211
|
+
`__unsafe_usePresenceContext` keeps its deliberate danger-prefix name via a
|
|
1212
|
+
justified scoped disable.
|
|
1213
|
+
- **`react-hooks/static-components`** (`layout`, `plugin-list`, `plugin-report`)
|
|
1214
|
+
— dynamic-icon / registry lookups (`resolveIcon`, `useRegistryComponent`) are
|
|
1215
|
+
stable component references, not components created during render → scoped
|
|
1216
|
+
disable with justification. `plugin-charts` `TreemapCell` was a _genuine_
|
|
1217
|
+
inline component and is hoisted to module scope (it is purely props-driven).
|
|
1218
|
+
- **`no-irregular-whitespace`** (`plugin-grid` `ImportWizard`) — the literal
|
|
1219
|
+
U+FEFF BOM prepended to exported CSV/text blobs (so Excel detects UTF-8) is
|
|
1220
|
+
now written as the `` escape: byte-identical at runtime, no literal
|
|
1221
|
+
irregular-whitespace character in source.
|
|
1222
|
+
- **`no-useless-assignment`** (`plugin-grid` `BulkActionDialog`) — dropped a
|
|
1223
|
+
dead `= null` initializer that the exhaustive `switch` (incl. `default`)
|
|
1224
|
+
overwrites before it is read.
|
|
1225
|
+
- **`no-unsafe-function-type`** (`plugin-view` `ViewTabBar`) — the dnd-kit
|
|
1226
|
+
render-prop `listeners` map is typed `Record<string, (...args: any[]) => void>`
|
|
1227
|
+
instead of bare `Function`.
|
|
1228
|
+
- **`no-require-imports`** (`plugin-kanban`, `plugin-view` tests) — hoisted
|
|
1229
|
+
`vi.mock` factories use an `async` factory with `await import('react')`.
|
|
1230
|
+
|
|
1231
|
+
- f80aaf2: **Distinguish writable `system` objects from engine-owned ones in the Console (framework ADR-0103 / #3220).** The framework split the overloaded `managedBy: 'system'` bucket: engine-owned rows stay read-only, but several `system` objects are admin/user-writable _data_ (Notification Preferences/Subscriptions/Templates, delegated RBAC assignments, user preferences) and declare `userActions` opening their writes.
|
|
1232
|
+
|
|
1233
|
+
The Console already surfaced the New/Edit/Delete buttons correctly for these (all affordance mirrors honour `userActions`), but the badge and empty-state _copy_ still called every `system` object a "read-only monitoring surface". Now:
|
|
1234
|
+
|
|
1235
|
+
- **`ManagedByBadge`** takes the object's `userActions` and, when a `system` object opens any write, renders the "Platform schema — admin-writable" variant instead of the engine-owned copy.
|
|
1236
|
+
- **`resolveManagedByEmptyState`** returns `undefined` for a `system` object whose `userActions.create` is set, so the generic empty state (with the New button) shows instead of "entries appear automatically".
|
|
1237
|
+
- New `managedByBadge.systemWritable.*` strings (en + zh; other locales fall back to the English default).
|
|
1238
|
+
|
|
1239
|
+
Copy/UX only — no behavioural change to what a user can do.
|
|
1240
|
+
|
|
1241
|
+
- 29c6040: fix(app-shell): redo the record-list "Add View" create flow — empty-name 405, invisible drafts, canonical naming
|
|
1242
|
+
|
|
1243
|
+
Rebuilds the record-list "Add View" / "Save as view" create path so a
|
|
1244
|
+
runtime-created view has one canonical identity and is actually verifiable
|
|
1245
|
+
before publish (supersedes #2754; fixes #2767).
|
|
1246
|
+
|
|
1247
|
+
- **Unified identity (P1).** New `viewEnvelope(objectName, spec, { name, label })`
|
|
1248
|
+
seam in `runtime-metadata-persistence.ts` emits the canonical ViewItem
|
|
1249
|
+
(`{ name: '<object>.<key>', object, viewKind: 'list', label, config }` with
|
|
1250
|
+
`config.data = { provider: 'object', object }`), mirroring the Studio
|
|
1251
|
+
`anchors.ts:createBuildBody`. The **qualified** name is passed as BOTH the
|
|
1252
|
+
`PUT /meta/view/:name` URL segment and `body.name`, so the `sys_metadata`
|
|
1253
|
+
row key, the ViewTabBar tab id, and the body identity all agree and the
|
|
1254
|
+
draft → read → publish loop resolves. `ObjectView` and `ObjectDataPage` both
|
|
1255
|
+
call the single helper — the duplicated envelope block is gone (P6).
|
|
1256
|
+
- **Empty-name guards (405).** `MetadataClient.save()` and
|
|
1257
|
+
`createRuntimeMetadata()` throw a clear contextual error instead of emitting
|
|
1258
|
+
`PUT /meta/view/` (empty `:name`, server 405).
|
|
1259
|
+
- **Draft visibility (P2/P3/P4).** `DataSource.listViews(objectName, { previewDrafts })`:
|
|
1260
|
+
in draft-preview mode the `ObjectStackAdapter` makes a **single**
|
|
1261
|
+
`MetadataClient.withPreviewDrafts(true).list('view')` request and uses the
|
|
1262
|
+
server's already-overlaid list (draft wins by name, `_draft` tagged) —
|
|
1263
|
+
replacing, not appending, so a draft that edits a published view can't
|
|
1264
|
+
double-tab. No hand-rolled `fetch` of metadata routes at the adapter layer.
|
|
1265
|
+
After a create in normal mode the console navigates to the new view with
|
|
1266
|
+
`?preview=draft`, so the DraftPreviewBar is visible and Publish is one click.
|
|
1267
|
+
- **CJK-aware naming (P5).** `CreateViewDialog` gains an editable machine-name
|
|
1268
|
+
field, prefilled via `slugify(label)` for Latin labels and required (submit
|
|
1269
|
+
disabled) when slugify yields empty for non-Latin labels — no more silent
|
|
1270
|
+
random `task_grid_mrsyt56j` names. New `console.objectView.viewName*` keys
|
|
1271
|
+
(en/zh).
|
|
1272
|
+
|
|
3
1273
|
## 16.0.0
|
|
4
1274
|
|
|
5
1275
|
### Patch Changes
|