@object-ui/components 16.1.0 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1666 -0
- package/README.md +28 -0
- package/dist/custom/field.d.ts +20 -2
- package/dist/custom/filter-builder.d.ts +98 -4
- package/dist/custom/index.d.ts +1 -0
- package/dist/custom/resizable.d.ts +31 -0
- package/dist/custom/sort-builder.d.ts +14 -3
- package/dist/index.css +147 -32
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4534 -3568
- package/dist/index.umd.cjs +6 -6
- package/dist/lib/lazy-icon.d.ts +10 -0
- package/dist/notifications/NotificationAlerts.d.ts +42 -0
- package/dist/notifications/NotificationBanners.d.ts +36 -0
- package/dist/notifications/NotificationInline.d.ts +41 -0
- package/dist/notifications/NotificationSnackbar.d.ts +36 -0
- package/dist/notifications/index.d.ts +26 -0
- package/dist/notifications/severity.d.ts +47 -0
- package/dist/renderers/complex/data-table.d.ts +7 -0
- package/dist/renderers/feedback/toaster.d.ts +9 -1
- package/dist/renderers/form/option-value.d.ts +32 -0
- package/dist/renderers/layout/page.d.ts +2 -2
- package/dist/renderers/layout/react-page.d.ts +2 -2
- package/dist/renderers/placeholders.d.ts +21 -0
- package/dist/share/ShareDialog.d.ts +23 -27
- package/dist/ui/index.d.ts +0 -1
- package/dist/ui/select.d.ts +22 -1
- package/package.json +44 -41
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1671 @@
|
|
|
1
1
|
# @object-ui/components
|
|
2
2
|
|
|
3
|
+
## 17.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4ae0ac4: One placement rule for action `locations` (objectui#3142).
|
|
8
|
+
|
|
9
|
+
**Breaking for metadata**: an action that declares no `locations` (missing key
|
|
10
|
+
or `[]`) no longer renders in a located surface. FROM: omitting `locations`
|
|
11
|
+
made an action appear on the list toolbar, the record header, and every
|
|
12
|
+
metadata-admin toolbar. TO: declare where it belongs —
|
|
13
|
+
`locations: ['record_header']` for the record header, `['list_toolbar']` for
|
|
14
|
+
the list toolbar, and so on. Nothing else changes; actions that already
|
|
15
|
+
declare a location are untouched.
|
|
16
|
+
|
|
17
|
+
Four renderers each answered "where does an action with no `locations` go?"
|
|
18
|
+
differently — `action:bar` and metadata-admin showed it EVERYWHERE,
|
|
19
|
+
`page:header` showed it on the header, `action:group` showed it for
|
|
20
|
+
`undefined` but hid it for `[]` — while `ActionEngine`, `RecordDetailView`,
|
|
21
|
+
`DeclaredActionsBar`, the related-list bridge and the environment toolbar all
|
|
22
|
+
showed it NOWHERE. The same action therefore appeared or vanished depending on
|
|
23
|
+
which component happened to draw it. All eight now go through one exported
|
|
24
|
+
predicate, `actionRendersAt(action, location)` from `@object-ui/types`: an
|
|
25
|
+
action renders at a location only if it declares that location.
|
|
26
|
+
|
|
27
|
+
The strict reading is the platform's own — ADR-0078 lists "an `action` with no
|
|
28
|
+
`locations`" as a verified inert shape, and the detail-page synthesizer already
|
|
29
|
+
documented "must include `locations: ['record_header']` to render". The
|
|
30
|
+
leniency contradicted both, and it is what let an aggregate-only bulk action
|
|
31
|
+
(objectui#3139) — one with no single-record placement by construction — mint a
|
|
32
|
+
list-toolbar button whose dispatch could only fail.
|
|
33
|
+
|
|
34
|
+
Two placements are declared elsewhere and need no `locations`, both unchanged:
|
|
35
|
+
host-injected chrome in the `systemActions` / `headerSystemActions` slot (now
|
|
36
|
+
consistently exempt on `page:header` too, where it used to be filtered), and an
|
|
37
|
+
action named in a view's `bulkActions` / `bulkActionDefs`.
|
|
38
|
+
|
|
39
|
+
Authoring side: Studio seeds `locations: ['record_header']` on a new action
|
|
40
|
+
instead of minting one that renders nowhere, and the action inspector says so
|
|
41
|
+
when no placement is ticked. The `ActionSchema.locations` JSDoc claimed a
|
|
42
|
+
`['record_header']` default that no renderer ever implemented — corrected.
|
|
43
|
+
|
|
44
|
+
- 09d30a4: Stop declaring 18 `@object-ui/auth` / `@object-ui/components` / `@object-ui/react`
|
|
45
|
+
symbols under names `@objectstack/spec` owns (objectui#3159, objectstack#4115
|
|
46
|
+
batch 5).
|
|
47
|
+
|
|
48
|
+
**Breaking for importers of all three packages** — six exported names changed,
|
|
49
|
+
because the spec exports the same name for a _different_ thing:
|
|
50
|
+
|
|
51
|
+
| package | was | now | what the spec's same-named export actually is |
|
|
52
|
+
| :----------- | :--------------------------- | :----------------------- | :----------------------------------------------------------------------------- |
|
|
53
|
+
| `auth` | `AuthSession` | `AuthClientSession` | the SERVER's session record (`{ id, userId, expiresAt: ISO string, token? }`) |
|
|
54
|
+
| `auth` | `AuthProviderConfig` | `AuthProviderOptions` | an OAuth/OIDC provider registration (`{ id, clientId, clientSecret, scope? }`) |
|
|
55
|
+
| `components` | `FilterCondition` | `FilterBuilderCondition` | the recursive ObjectQL predicate AST (`$and`/`$or`/`$not`) |
|
|
56
|
+
| `components` | `Field` | `FieldContainer` | an object FIELD's metadata and its builder namespace |
|
|
57
|
+
| `react` | `ConflictResolutionStrategy` | `ConflictResolution` | the metadata-MERGE policy (`error \| priority \| first-wins \| last-wins`) |
|
|
58
|
+
|
|
59
|
+
The `react` rename is the odd one out: the new name is the **spec's own** name
|
|
60
|
+
for the union that hook always used, so it is a re-export rather than a dialect.
|
|
61
|
+
|
|
62
|
+
Eleven more keep their names and are now **imported or derived from the spec**
|
|
63
|
+
instead of re-declared: `TenancyPosture`, `DelegableScope` (+`DelegableAdminScope`),
|
|
64
|
+
`AuthUser`, `ShareLinkPermission`, `ShareLinkAudience`, `ShareLink`, `SortItem`,
|
|
65
|
+
`OfflineStrategy`, `OfflineCacheConfig`, `OfflineSyncConfig`, `OfflineConfig`,
|
|
66
|
+
`NavigationConfig`.
|
|
67
|
+
|
|
68
|
+
**Three of the copies were losing information, not just duplicating it.**
|
|
69
|
+
|
|
70
|
+
- `AuthUser` never declared the spec's `positions` or `tenantId` — the
|
|
71
|
+
authorization inputs. Its `[key: string]: unknown` index signature meant the
|
|
72
|
+
omission was invisible at every call site _and_ to any structural comparison
|
|
73
|
+
(the objectstack#4075 mechanism). It now `extends` the spec principal, so the
|
|
74
|
+
display-only fields (`image`, `role`, `roles`, `emailVerified`) are the delta
|
|
75
|
+
and the spec's keys arrive on their own.
|
|
76
|
+
- `useNavigationOverlay`'s copy carried the note _"inline … to avoid importing
|
|
77
|
+
from `@object-ui/types` (which may not be a direct dependency of
|
|
78
|
+
`@object-ui/react`)"_. The vocabulary belongs to `@objectstack/spec`, which
|
|
79
|
+
**is** a direct dependency — the same expired "kept local to avoid a
|
|
80
|
+
dependency" comment objectui#3169 found in `@object-ui/app-shell`.
|
|
81
|
+
- `useOffline` and `usePerformance` both opened with _"Types aligned with
|
|
82
|
+
`@objectstack/spec` v2.0.7"_. The installed spec is 17.0.0-rc.1.
|
|
83
|
+
|
|
84
|
+
`ShareLink` derives from the spec row **minus `password_hash`** — omitted rather
|
|
85
|
+
than optional, because it is the credential itself and typing it in a browser
|
|
86
|
+
package is an invitation to render it. `password_protected` (the boolean the UI
|
|
87
|
+
needs in its place) is the one local addition.
|
|
88
|
+
|
|
89
|
+
The config types derive from each schema's **input** side, not `z.infer`.
|
|
90
|
+
`useOffline(config: OfflineConfig = {})` defaults to the empty object, which the
|
|
91
|
+
output type — every `.default()`ed key required — would reject outright.
|
|
92
|
+
|
|
93
|
+
`@objectstack/spec` moves from `devDependencies` to `dependencies` in
|
|
94
|
+
`@object-ui/components`: its public type surface now references the spec.
|
|
95
|
+
|
|
96
|
+
Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
|
|
97
|
+
tracks `@objectstack`, so breaking changes of our own ship as minor with the
|
|
98
|
+
semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
|
|
99
|
+
all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.
|
|
100
|
+
|
|
101
|
+
- 726b89c: `@object-ui/types` stops declaring sixteen symbols under names `@objectstack/spec` owns (objectui#3156, objectstack#4115).
|
|
102
|
+
|
|
103
|
+
Seven are now **derived** from the spec, nine are **renamed** to the local
|
|
104
|
+
dialect they always were. Both halves remove the same hazard: a local
|
|
105
|
+
declaration under a spec export's name reads as the spec's own definition to
|
|
106
|
+
the next reader, so a copy that is merely _correct today_ is a planted premise
|
|
107
|
+
tomorrow.
|
|
108
|
+
|
|
109
|
+
**Derived** — the spec now supplies the keys, by reference:
|
|
110
|
+
|
|
111
|
+
| symbol | derivation |
|
|
112
|
+
| :----------------------- | :-------------------------------------------------------------------------------- |
|
|
113
|
+
| `ActionParam` | `z.input<typeof ActionParamSchema>`, `type` widened to the local legacy spellings |
|
|
114
|
+
| `CreateExportJobRequest` | `Omit<CreateExportJobInput, 'object'>` (`object` is the method argument) |
|
|
115
|
+
| `CreateExportJobResult` | re-export from `@objectstack/spec/contracts` |
|
|
116
|
+
| `ImportRowResult` | re-export from `@objectstack/spec/api` |
|
|
117
|
+
| `NavigationArea` | spec keys, with `navigation` / `visible` pinned locally |
|
|
118
|
+
| `NavigationAreaSchema` | `specFieldsExcept(NavigationAreaSchema.shape, …)` |
|
|
119
|
+
| `Theme` | re-export of the spec's `ThemeInput` (the authoring shape) |
|
|
120
|
+
| `ExportJobFormat` | re-export of the spec's `ExportFormat` |
|
|
121
|
+
|
|
122
|
+
Four of these close real gaps rather than tidy names. `ActionParam` never
|
|
123
|
+
declared `reference` — the key `resolveActionParams()` actually reads for an
|
|
124
|
+
inline lookup target — nor `defaultFromRow`, which the metadata designer's own
|
|
125
|
+
inspector writes; it also narrowed `visible` to a bare string although the
|
|
126
|
+
resolver has always accepted the `{ dialect, source }` envelope too.
|
|
127
|
+
`CreateExportJobResult.createdAt` and `ImportRowResult.action` were optional
|
|
128
|
+
here and required by the server, leaving every consumer a branch that could
|
|
129
|
+
never run. And `NavigationArea`'s `id` now carries the spec's own length rule
|
|
130
|
+
instead of accepting any string.
|
|
131
|
+
|
|
132
|
+
**Renamed** — same word, different concept:
|
|
133
|
+
|
|
134
|
+
| was | now | why |
|
|
135
|
+
| :----------------- | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
|
|
136
|
+
| `FileMetadata` | `UploadedFileMetadata` | field-VALUE payload (`url`, `original_name`), not the storage file record |
|
|
137
|
+
| `GestureType` | `TouchGestureType` | direction-fused (`swipe-left`), not the spec's type+direction pair |
|
|
138
|
+
| `GestureConfig` | `TouchGestureConfig` | gesture→`action` binding, not per-gesture tuning |
|
|
139
|
+
| `OfflineConfig` | `PWAOfflineConfig` | service-worker route caching, not the offline data/sync model |
|
|
140
|
+
| `PageRegion` | `PageNodeRegion` | region of the renderer page NODE, holding `SchemaNode`s |
|
|
141
|
+
| `PageRegionSchema` | `PageNodeRegionSchema` | zod twin of the above |
|
|
142
|
+
| `ResponsiveConfig` | `MobileResponsiveConfig` | mobile box config, not the spec's SDUI grid contract |
|
|
143
|
+
| `WidgetManifest` | `RuntimeWidgetManifest` | SDUI component manifest, not the field-widget plugin manifest |
|
|
144
|
+
| `WidgetSource` | `RuntimeWidgetSource` | `module`/`inline`/`registry` loader union — and its `inline` carries a resolved component where the spec's carries source code |
|
|
145
|
+
|
|
146
|
+
**Migration**: the old names are gone, not deprecated — an alias would preserve
|
|
147
|
+
exactly the ambiguity being removed. Import the new name; nothing about the
|
|
148
|
+
shapes changed. `@object-ui/types` already re-exports the spec's own
|
|
149
|
+
`SpecResponsiveConfig`, and `@object-ui/react`'s `useOffline` config remains the
|
|
150
|
+
spec-shaped `OfflineConfig`, so both concepts stay reachable under
|
|
151
|
+
distinguishable names.
|
|
152
|
+
|
|
153
|
+
Each rename carries a bidirectional tripwire
|
|
154
|
+
(`packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts`): it fails if
|
|
155
|
+
the spec ever claims the new name, and also if the spec retires the old one —
|
|
156
|
+
at which point the natural name can be taken back rather than the workaround
|
|
157
|
+
outliving its reason.
|
|
158
|
+
|
|
159
|
+
### Patch Changes
|
|
160
|
+
|
|
161
|
+
- cb82705: A standalone grid's search box searches the list, not the page you can see (objectui#3118).
|
|
162
|
+
|
|
163
|
+
Under server-side pagination a standalone `ObjectGrid` rendered `data-table`'s
|
|
164
|
+
built-in search box, and that box filtered the rows the table was holding —
|
|
165
|
+
which is one page. The user read "2 results for X in this list" while 3075 rows
|
|
166
|
+
never participated, with the pager beside it still reading `1 / 63`. Every piece
|
|
167
|
+
was individually correct: `searchable` defaults to true, `manualPagination` is
|
|
168
|
+
true, and the two are declared next to each other in the same object literal.
|
|
169
|
+
|
|
170
|
+
This is objectui#3106 one axis over — sort there, filter here — and it takes the
|
|
171
|
+
same shape. `DataTable` gains `manualSearch` + a controlled `search` +
|
|
172
|
+
`onSearchChange`. In that mode it filters nothing, reports the typed term, and
|
|
173
|
+
renders `search` as the box's value, holding **no** term of its own: a private
|
|
174
|
+
copy beside a controlled prop is the shape the defect had. `ObjectGrid` turns
|
|
175
|
+
that term into a `$search` on the refetch — the server picks the matching fields
|
|
176
|
+
from the object's metadata (ADR-0061), the same channel the ListView toolbar has
|
|
177
|
+
always used — and returns to page 1, since a new term makes the old page index a
|
|
178
|
+
different set of rows (usually no rows at all). `$searchFields` rides along only
|
|
179
|
+
when the view declared `searchableFields`, which can narrow the server-resolved
|
|
180
|
+
set and never widen it.
|
|
181
|
+
|
|
182
|
+
Two things worth naming:
|
|
183
|
+
|
|
184
|
+
- Both paths are never live at once. The server's answer is the answer; a client
|
|
185
|
+
pass left running underneath would silently re-narrow it to whichever returned
|
|
186
|
+
rows happen to contain the term as _rendered text_, overruling the server's
|
|
187
|
+
own notion of which fields are searchable.
|
|
188
|
+
- Under `manualSearch` a table with no `onSearchChange` renders **no** search
|
|
189
|
+
box. The sort axis could degrade to inert headers; here there is no honest
|
|
190
|
+
local behaviour to fall back to, because the rows to search are not in the
|
|
191
|
+
browser.
|
|
192
|
+
|
|
193
|
+
Client-paginated grids are untouched: inline, bound and grouped grids hold every
|
|
194
|
+
row they display, so their box keeps filtering in memory, where the count it
|
|
195
|
+
produces is true. The ListView path was never affected — it passes
|
|
196
|
+
`showSearch: false` and searches from its own toolbar.
|
|
197
|
+
|
|
198
|
+
- f6e8d78: Lookup search inside a create/edit modal is typeable again (objectui#3183).
|
|
199
|
+
|
|
200
|
+
In every production console build, the search input of a lookup field's
|
|
201
|
+
quick-select popover — and the nested Record Picker dialog — could not take
|
|
202
|
+
focus while the form modal was open: every click/focus was synchronously
|
|
203
|
+
yanked back to the field trigger, so a lookup could not be searched while
|
|
204
|
+
creating a record.
|
|
205
|
+
|
|
206
|
+
Root cause is a race in stock `@radix-ui/react-focus-scope@1.1.16`: the
|
|
207
|
+
focus-scopes stack effect's cleanup schedules `focusScopesStack.remove(scope)`
|
|
208
|
+
in a `setTimeout(0)`. When the effect re-runs for a still-mounted scope (a
|
|
209
|
+
`container` ref flicker), the re-run re-`add`s the scope and the stale timeout
|
|
210
|
+
then evicts it — the dialog's trap listeners stay active but its scope is no
|
|
211
|
+
longer in the stack, so an opening popover pauses nothing and the trap yanks
|
|
212
|
+
focus out of the popover forever.
|
|
213
|
+
|
|
214
|
+
Fixed via `patches/@radix-ui__react-focus-scope.patch`: an effect re-run for a
|
|
215
|
+
live scope cancels the pending eviction; a real unmount still runs the full
|
|
216
|
+
delayed cleanup (autofocus-on-unmount + stack removal). Regression-tested in
|
|
217
|
+
`packages/components` with a deterministic reproduction of the race.
|
|
218
|
+
|
|
219
|
+
- a8ad6c0: A required boolean must be savable in its UNCHECKED state — `false` and `0` are values.
|
|
220
|
+
|
|
221
|
+
Reported against an AI-built task tracker whose 任务 object has a required
|
|
222
|
+
`是否完成` boolean: the create form showed the switch OFF, answered "是否完成不能
|
|
223
|
+
为空", and saved instantly once the switch was turned ON. The app could only ever
|
|
224
|
+
create ALREADY-DONE tasks — the one state the control shows by default was the
|
|
225
|
+
one value it refused to save (cloud#972).
|
|
226
|
+
|
|
227
|
+
Two defects stacked, and either alone is enough to break it:
|
|
228
|
+
|
|
229
|
+
**The `required` verdict read truthiness, not presence.** `@objectstack/spec`
|
|
230
|
+
FieldSchema.required (ADR-0113) is "an insert must provide a NON-NULL value",
|
|
231
|
+
and objectql's record validator implements exactly that. react-hook-form's
|
|
232
|
+
built-in rule instead fails whenever `isBoolean(value) && !value` — its
|
|
233
|
+
accept-the-terms checkbox heritage — silently redefining every required boolean
|
|
234
|
+
as "must be TRUE", including a select whose chosen option value is `false`. It
|
|
235
|
+
also disagreed the other way, letting a whitespace-only string through for the
|
|
236
|
+
server to reject with a 400. The form renderer no longer hands RHF its own
|
|
237
|
+
`required`: the check is now a `validate` entry keyed `required` (so the error
|
|
238
|
+
still surfaces as `type: 'required'`, which the conditional-required cleanup
|
|
239
|
+
keys on) backed by a new shared `isMissingForRequired` in `@object-ui/core`, a
|
|
240
|
+
deliberate mirror of objectql `record-validator.isMissing` — `undefined`,
|
|
241
|
+
`null`, blank-after-trim string, empty array. Deleting the inherited rule also
|
|
242
|
+
stops a `required` that rode in on `validation` from outliving a `requiredWhen`
|
|
243
|
+
that resolved to FALSE.
|
|
244
|
+
|
|
245
|
+
**A boolean field held `undefined` while displaying "off".** A two-state control
|
|
246
|
+
has no third state, but a field with no entry in `defaultValues` rendered an OFF
|
|
247
|
+
switch backed by nothing: the create payload omitted the column (it lands null,
|
|
248
|
+
which reads as unchecked but isn't) and the presence check above would still
|
|
249
|
+
refuse it. The form renderer now folds `false` into `defaultValues` for every
|
|
250
|
+
boolean-widget field the caller left unset — in `defaultValues` itself, not
|
|
251
|
+
per-Controller, because that object is also the dirty-check baseline and what
|
|
252
|
+
the defaults-reset window replays. Every surface gets it, including the
|
|
253
|
+
modal/drawer create dialogs that start from a bare `{}`. An authored default
|
|
254
|
+
(or a loaded record, `null` included) still wins.
|
|
255
|
+
|
|
256
|
+
`WizardForm`'s cross-step gate had its own copy of the empty-value predicate; it
|
|
257
|
+
now imports the shared one so it cannot drift from the per-field verdict. And
|
|
258
|
+
the field-demo renderer read `schema.defaultValue || schema.value`, throwing
|
|
259
|
+
away an authored default of `false` / `0` / `''` — same falsy-as-empty class,
|
|
260
|
+
now `??`.
|
|
261
|
+
|
|
262
|
+
Verified end to end on a local stack against the exact metadata shape
|
|
263
|
+
`apply_blueprint` materializes (`{ type: 'boolean', required: true }`, no
|
|
264
|
+
default): a 是否完成 = 否 task with 工时 = 0 now creates and persists as
|
|
265
|
+
`{ hours: 0, is_done: false }`, turning the switch on still stores `true`, and a
|
|
266
|
+
blank required text is still refused.
|
|
267
|
+
|
|
268
|
+
- Updated dependencies [4ae0ac4]
|
|
269
|
+
- Updated dependencies [696e3c1]
|
|
270
|
+
- Updated dependencies [bca45cc]
|
|
271
|
+
- Updated dependencies [a889e31]
|
|
272
|
+
- Updated dependencies [09d30a4]
|
|
273
|
+
- Updated dependencies [4bf612c]
|
|
274
|
+
- Updated dependencies [335041c]
|
|
275
|
+
- Updated dependencies [b414983]
|
|
276
|
+
- Updated dependencies [256f8cc]
|
|
277
|
+
- Updated dependencies [d9668a7]
|
|
278
|
+
- Updated dependencies [4b470b9]
|
|
279
|
+
- Updated dependencies [cb82705]
|
|
280
|
+
- Updated dependencies [f572849]
|
|
281
|
+
- Updated dependencies [4a51e77]
|
|
282
|
+
- Updated dependencies [ea96284]
|
|
283
|
+
- Updated dependencies [d3584c6]
|
|
284
|
+
- Updated dependencies [cc70b8f]
|
|
285
|
+
- Updated dependencies [a8ad6c0]
|
|
286
|
+
- Updated dependencies [444457c]
|
|
287
|
+
- Updated dependencies [850033c]
|
|
288
|
+
- Updated dependencies [022e4c3]
|
|
289
|
+
- Updated dependencies [009e25d]
|
|
290
|
+
- Updated dependencies [726b89c]
|
|
291
|
+
- @object-ui/types@17.2.0
|
|
292
|
+
- @object-ui/core@17.2.0
|
|
293
|
+
- @object-ui/react@17.2.0
|
|
294
|
+
- @object-ui/i18n@17.2.0
|
|
295
|
+
- @object-ui/sdui-parser@17.2.0
|
|
296
|
+
- @object-ui/react-runtime@17.2.0
|
|
297
|
+
|
|
298
|
+
## 17.1.0
|
|
299
|
+
|
|
300
|
+
### Minor Changes
|
|
301
|
+
|
|
302
|
+
- 9e7349e: **`target` is the only action handler slot — the `execute` alias is gone from the renderer (framework#3856).**
|
|
303
|
+
|
|
304
|
+
`ActionRunner.executeScript` read `action.target || action.execute`. That fallback
|
|
305
|
+
is unreachable against `@objectstack/spec` 17: `execute` is now a tombstoned key
|
|
306
|
+
(framework#3855) that the parser **rejects** with the rename prescription, so no
|
|
307
|
+
parsed action can carry it and the `||` could only ever yield `target`. Verified
|
|
308
|
+
against 17.0.0-rc.0 — an action declaring `execute` fails `ActionSchema.safeParse`,
|
|
309
|
+
and a `target` action's parsed output has no `execute` key at all.
|
|
310
|
+
|
|
311
|
+
Deleted rather than left as harmless residue: two handler slots is what let one
|
|
312
|
+
action run one script server-side and a different one client-side (framework#3713,
|
|
313
|
+
where this renderer preferred the alias while the spec transform preferred
|
|
314
|
+
`target`). A dead slot still reads as a live contract to the next maintainer.
|
|
315
|
+
|
|
316
|
+
`execute` is also **removed from the types**, which is the part that had never
|
|
317
|
+
landed. framework#3856 predicted a compile error here; there wasn't one, because
|
|
318
|
+
neither reader was typed against the spec's `z.infer`:
|
|
319
|
+
|
|
320
|
+
- `@object-ui/types` `ActionSchema` hand-declared `execute?: string`. Removed, so
|
|
321
|
+
`execute: '…'` now fails `tsc` at the authoring site (TS2353).
|
|
322
|
+
- `@object-ui/core` `ActionDef` hand-declared it too. Removed — but `ActionDef`
|
|
323
|
+
carries a `[key: string]: any` index signature, so stale hand-authored metadata
|
|
324
|
+
that never passed through the parser still compiles. For that path
|
|
325
|
+
`executeScript` now returns the rename prescription instead of a bare
|
|
326
|
+
"No script provided", matching the spec tombstone's rule that removing an
|
|
327
|
+
authorable key must be audible: silently binding no handler is the
|
|
328
|
+
"Mark Done does nothing" shape (framework#2169).
|
|
329
|
+
|
|
330
|
+
The four action renderers (`action:button`, `action:icon`, `action:menu`,
|
|
331
|
+
`action:group`) no longer forward `execute` into the runner, and Studio's
|
|
332
|
+
`ActionPreview` no longer falls back to it — previewing an alias-only draft as
|
|
333
|
+
"bound" contradicted the parse that rejects it on save.
|
|
334
|
+
|
|
335
|
+
Requires `@objectstack/spec` 17. Metadata still on the alias is rewritten by
|
|
336
|
+
`os migrate meta --from 16`.
|
|
337
|
+
|
|
338
|
+
- 38ca8be: refactor(fields): `requiredWhen` is the only required-predicate slot — drop the retired `conditionalRequired` alias
|
|
339
|
+
|
|
340
|
+
`@objectstack/spec` 17 (objectstack#3855) **retired** `Field.conditionalRequired`,
|
|
341
|
+
the long-deprecated alias of `requiredWhen`. ObjectUI carried a back-compat read
|
|
342
|
+
for it in seven places; all of them are removed.
|
|
343
|
+
|
|
344
|
+
The removal is safe because the spec did not merely _stop emitting_ the key — it
|
|
345
|
+
made authoring it **fail loudly**. `retiredKey()` declares the key as
|
|
346
|
+
`z.never()`, so:
|
|
347
|
+
|
|
348
|
+
- `z.input` types it as `never` — writing it is a `tsc` error at the authoring site;
|
|
349
|
+
- the parse **rejects** it (verified against `17.0.0-rc.0`), at both `FieldSchema`
|
|
350
|
+
and `ObjectSchema`, with the prescription as the message:
|
|
351
|
+
|
|
352
|
+
> `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use
|
|
353
|
+
> `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged.
|
|
354
|
+
> Run `os migrate meta --from 16` to rewrite it automatically.
|
|
355
|
+
|
|
356
|
+
So spec-parsed metadata cannot carry the key — an object declaring it fails to
|
|
357
|
+
load rather than loading with the rule silently dropped. Keeping a renderer-side
|
|
358
|
+
`requiredWhen ?? conditionalRequired` would have re-created exactly the second
|
|
359
|
+
de-facto contract the tombstone exists to prevent: the key would have kept
|
|
360
|
+
working in the UI while being rejected everywhere else, hiding the producer's bug
|
|
361
|
+
(AGENTS.md #0.1). "Backend-agnostic" (#1) does not argue for keeping it either —
|
|
362
|
+
`conditionalRequired` is an ObjectStack-spec-ism, so the only producers that ever
|
|
363
|
+
emit it are ObjectStack producers on ≤16, and the spec ships them a converter.
|
|
364
|
+
|
|
365
|
+
Removed from:
|
|
366
|
+
|
|
367
|
+
| package | site |
|
|
368
|
+
| :----------------------- | :-------------------------------------------------------------------------------------------------------- |
|
|
369
|
+
| `@object-ui/types` | the `conditionalRequired?:` member on `FormField` |
|
|
370
|
+
| `@object-ui/core` | the `??` fallback + rules-param member in `resolveFieldRuleState` |
|
|
371
|
+
| `@object-ui/components` | three pass-throughs in the form renderer |
|
|
372
|
+
| `@object-ui/plugin-form` | `ObjectForm`, `ModalForm`, `sectionFields`, `deriveMasterDetail` (×2) |
|
|
373
|
+
| `@object-ui/app-shell` | the field inspector's legacy read/auto-migrate, and the key's entry in `clientValidation`'s CEL lint list |
|
|
374
|
+
|
|
375
|
+
**Studio authors lose nothing.** The object designer's draft validation parses
|
|
376
|
+
against the spec's own `ObjectSchema`, so a draft carrying the key now surfaces
|
|
377
|
+
the tombstone's rename prescription under the same `fields.<name>.conditionalRequired`
|
|
378
|
+
path the CEL lint used to report — a better message than the inspector's silent
|
|
379
|
+
auto-migration, and one the server agrees with. That behavior is pinned by a test.
|
|
380
|
+
|
|
381
|
+
**Migrating:** rename the key to `requiredWhen` (the CEL value is unchanged), or
|
|
382
|
+
run `os migrate meta --from 16`.
|
|
383
|
+
|
|
384
|
+
- 07de839: fix(notifications): the config, `position` and action `variant` are read instead of forked or ignored (#3014 follow-up)
|
|
385
|
+
|
|
386
|
+
The last of the notification contract. After `displayType` (#3071) and `icon`
|
|
387
|
+
(#3076), four gaps of the same family were left:
|
|
388
|
+
|
|
389
|
+
- **the config was 3/4 inert** — only `defaultDuration` was ever read.
|
|
390
|
+
`maxVisible` and `stacking` were carried and ignored, while
|
|
391
|
+
`NotificationBanners` capped at a hard-coded `3` of its own;
|
|
392
|
+
- **its field names forked from `NotificationConfigSchema`** — `position` vs
|
|
393
|
+
`defaultPosition`, a renderer-local `stacking` boolean with no spec
|
|
394
|
+
counterpart, and no `pauseOnHover` at all;
|
|
395
|
+
- **a notification could not declare a `position`.** The #3008 parity guard
|
|
396
|
+
asserted the position _vocabulary_ matched the spec while nothing positioned
|
|
397
|
+
anything by it — a guard passing over an unused value;
|
|
398
|
+
- **`NotificationActionButton.variant` was the shadcn Button vocabulary**
|
|
399
|
+
(`default | destructive | outline`) under a spec-shaped name, forking
|
|
400
|
+
`NotificationActionSchema.variant` (`primary | secondary | link`).
|
|
401
|
+
|
|
402
|
+
**How positioning resolves now** — `notification.position ?? config.defaultPosition
|
|
403
|
+
?? nothing`, and "nothing" is a real answer:
|
|
404
|
+
|
|
405
|
+
- **declared** → the surface pins itself there, always. `presentNotificationToast`
|
|
406
|
+
passes it per-toast so the contract wins over the container;
|
|
407
|
+
- **undeclared** → the surface keeps its own anchor (a snackbar's bottom edge) or
|
|
408
|
+
defers to the host's toast chrome.
|
|
409
|
+
|
|
410
|
+
That asymmetry is the design decision. The host's sonner container also serves
|
|
411
|
+
toasts that are _not_ spec notifications (the console action runtime's own
|
|
412
|
+
`toast.*` calls), so it stays the fallback authority for placement — never a
|
|
413
|
+
competing one. A declared position a component prop could silently override
|
|
414
|
+
would be the same "validates, then does nothing" shape this whole area is about.
|
|
415
|
+
Hence `defaultPosition` has no fabricated default: "the host didn't say" has to
|
|
416
|
+
be representable.
|
|
417
|
+
|
|
418
|
+
Also: `maxVisible` / `stackDirection` now drive every stacking surface through
|
|
419
|
+
one shared `visibleNotificationStack` (cap keeps the NEWEST, stack grows in the
|
|
420
|
+
declared direction); `pauseOnHover` holds a transient notification's timer and
|
|
421
|
+
resumes it with the time it had left, which needed the provider to track live
|
|
422
|
+
timers rather than fire-and-forget `setTimeout`s. Legacy spellings still resolve:
|
|
423
|
+
`position` folds into `defaultPosition`, and `stacking: false` reads as
|
|
424
|
+
`maxVisible: 1` rather than being ignored.
|
|
425
|
+
|
|
426
|
+
`onToast` now receives the resolved config as a second argument, so the delegate
|
|
427
|
+
can apply the parts of the contract only it can. Existing one-argument handlers
|
|
428
|
+
are unaffected. The spec-parity guard gained the action-variant vocabulary, the
|
|
429
|
+
one notification enum it did not cover.
|
|
430
|
+
|
|
431
|
+
- 2a40b5e: feat(notifications): each spec `displayType` gets its own presentation instead of a toast (#3014)
|
|
432
|
+
|
|
433
|
+
#3008 closed the **contract** half of this: `NotificationContext`'s union matched
|
|
434
|
+
`NotificationTypeSchema`, and `notify()` materialized the declared type so a
|
|
435
|
+
consumer _could_ branch on it. Nothing did. `NotificationProvider` handed every
|
|
436
|
+
item to the host's `onToast` delegate regardless of type, so an author picking
|
|
437
|
+
`banner` or `inline` got a transient overlay — plausible output, wrong output.
|
|
438
|
+
|
|
439
|
+
Each of the five spec types now has a presentation of its own:
|
|
440
|
+
|
|
441
|
+
| `displayType` | Presentation | Rendered by |
|
|
442
|
+
| ------------- | ------------------------------------------------------ | ---------------------------------- |
|
|
443
|
+
| `toast` | transient overlay (unchanged) | the host's `onToast` delegate |
|
|
444
|
+
| `snackbar` | bottom-anchored bar, one at a time, at most one action | `<NotificationSnackbar />` |
|
|
445
|
+
| `banner` | page-width strip **in the content flow** | `<NotificationBanners />` |
|
|
446
|
+
| `alert` | blocking acknowledgement dialog, FIFO queue | `<NotificationAlerts />` |
|
|
447
|
+
| `inline` | in place, at the raising surface | `<NotificationInline scope="…" />` |
|
|
448
|
+
|
|
449
|
+
The four surface components ship from `@object-ui/components` and subscribe via
|
|
450
|
+
`useNotificationsByPresentation(type, scope?)`.
|
|
451
|
+
|
|
452
|
+
**Answers to the three questions the issue left open:**
|
|
453
|
+
|
|
454
|
+
1. **Banner/inline placement is the host's.** They are not overlays: a banner takes
|
|
455
|
+
space at the top of the content area and an `inline` notification belongs next to
|
|
456
|
+
the thing that raised it. So the context exposes the items and the surfaces
|
|
457
|
+
subscribe, rather than one `onToast`-style delegate positioning everything. An
|
|
458
|
+
`inline` notification carries a `scope` that pairs it with its outlet, so two
|
|
459
|
+
forms on one page don't show each other's messages.
|
|
460
|
+
2. **`alert` is modal-ish but NOT the action system's `ModalHandler`.** That handler
|
|
461
|
+
resolves a page/object, renders it, and reports an `ActionResult` back to the
|
|
462
|
+
`ActionRunner`; a notification alert has no schema, no target and no result.
|
|
463
|
+
Routing it there would mean synthesizing a page just to say "OK". It renders
|
|
464
|
+
through the `AlertDialog` primitive instead — no second action-modal path.
|
|
465
|
+
3. **`snackbar` earns its own component.** It supersedes rather than stacks, anchors
|
|
466
|
+
bottom regardless of the toast position config, and takes at most one action.
|
|
467
|
+
Making it a sonner variant is what "presents as a toast" means.
|
|
468
|
+
|
|
469
|
+
**Also fixed:** auto-dismiss now follows the presentation. `toast`/`snackbar` keep
|
|
470
|
+
the transient timer; `banner`/`alert`/`inline` are persistent unless the raiser sets
|
|
471
|
+
`duration` explicitly — a persistent banner used to evaporate on the shared 5s toast
|
|
472
|
+
timer. `dismissible` is honored on the persistent surfaces (an `alert` always keeps
|
|
473
|
+
its acknowledge button; `dismissible: false` only closes the Escape route).
|
|
474
|
+
|
|
475
|
+
`onToast` now receives **only** `toast` items. A provider with no `onToast` remains
|
|
476
|
+
the supported store-only mode (a bell reading `notifications`/`unreadCount`), but
|
|
477
|
+
raising one of the other four types with its surface unmounted warns in dev, naming
|
|
478
|
+
the component to mount — that failure used to be silent.
|
|
479
|
+
|
|
480
|
+
`NOTIFICATION_PRESENTATIONS` is typed `Record<NotificationPresentation, …>`, so a new
|
|
481
|
+
member in the spec enum fails type-check until its presentation is decided; a parity
|
|
482
|
+
test additionally asserts the table covers `NotificationTypeSchema` exactly and that
|
|
483
|
+
no two types share a surface.
|
|
484
|
+
|
|
485
|
+
- df613fa: fix(notifications): the spec `icon` is read instead of stored and ignored (#3014 follow-up)
|
|
486
|
+
|
|
487
|
+
`NotificationSchema.icon` — "Icon name override" — reached `NotificationItem` and
|
|
488
|
+
stopped there. Every surface drew the severity icon, so an author writing
|
|
489
|
+
`icon: 'rocket'` got the success checkmark. Same shape as the `displayType`
|
|
490
|
+
collapse #3071 fixed: a value that validates, is carried, and renders nothing.
|
|
491
|
+
|
|
492
|
+
All five presentations now resolve it through one rule (`notificationIcon`): a
|
|
493
|
+
declared Lucide name — kebab-case or PascalCase — replaces the severity icon;
|
|
494
|
+
anything else falls back to it. That includes the console's sonner toast, so the
|
|
495
|
+
override behaves identically on a toast, a banner, a snackbar, an alert and an
|
|
496
|
+
inline message.
|
|
497
|
+
|
|
498
|
+
**The fallback is the interesting part.** `getLazyIcon` degrades an unknown name
|
|
499
|
+
to a `Database` glyph, which is right for a data-shaped schema slot and wrong
|
|
500
|
+
here — on an error notification it swaps a meaningful icon for a meaningless one.
|
|
501
|
+
So the name is checked first, via a new `isLucideIconName` export, and a typo
|
|
502
|
+
costs the author their override and nothing more.
|
|
503
|
+
|
|
504
|
+
- 0ded602: fix(form): a server rejection that names fields now marks those fields (objectstack#3896)
|
|
505
|
+
|
|
506
|
+
The server has always said which field it rejected. `@objectstack/objectql`'s
|
|
507
|
+
validators throw `VALIDATION_FAILED` with `fields[]` — one entry per offending
|
|
508
|
+
field, each with a human `message` — and both the REST layer and the runtime
|
|
509
|
+
dispatcher serve that as a 400 with the entries intact.
|
|
510
|
+
|
|
511
|
+
Every form dropped them. The submit handler caught the rejection, ran the
|
|
512
|
+
message through `extractWriteErrorMessage`, and showed **one undirected toast**:
|
|
513
|
+
the user was told something was wrong but not _what_, on a surface that already
|
|
514
|
+
knows how to mark an input — and already does exactly that for client-side
|
|
515
|
+
validation. On a long form the offending field was often off-screen, so "创建"
|
|
516
|
+
appeared to do nothing.
|
|
517
|
+
|
|
518
|
+
**Now the two failures behave identically, because they share one
|
|
519
|
+
implementation.** The per-field marking, the toast naming the fields, and the
|
|
520
|
+
scroll-and-focus of the first offender (#2793) were extracted from the
|
|
521
|
+
client-side invalid handler; the server path calls the same function. As far as
|
|
522
|
+
the person filling in the form is concerned these are the same event — only the
|
|
523
|
+
referee differs.
|
|
524
|
+
|
|
525
|
+
Three layers, each of which was dropping the detail:
|
|
526
|
+
|
|
527
|
+
- **`@object-ui/react`** — new `extractFieldErrors(err)` (exported alongside
|
|
528
|
+
`extractWriteErrorMessage` / `isPermissionError`) normalises the three shapes
|
|
529
|
+
the error can arrive in: a typed `ValidationError` from the ObjectStack
|
|
530
|
+
adapter, the raw `@objectstack/client` error (whose `details` falls back to the
|
|
531
|
+
whole response body, which is where `fields[]` lands), and a hand-rolled error
|
|
532
|
+
carrying `fields` directly — the server duck-types that shape identically, so
|
|
533
|
+
the client must not be pickier than the server. Entries with no usable `field`
|
|
534
|
+
are **dropped rather than guessed at**: marking an innocent input is worse than
|
|
535
|
+
the generic toast.
|
|
536
|
+
- **`@object-ui/data-objectstack`** — `normaliseClientError` now maps a 400
|
|
537
|
+
`VALIDATION_FAILED` onto the `ValidationError` class that has sat in
|
|
538
|
+
`errors.ts` since the package was written, exported and **never once
|
|
539
|
+
constructed**. Its `validationErrors: Array<{ field, message }>` shape was
|
|
540
|
+
already exactly right. `create` also now normalises at all: only `update` did,
|
|
541
|
+
so a rejected insert reached callers as the raw client error — and a create is
|
|
542
|
+
the path that most often trips required-field validation.
|
|
543
|
+
- **`@object-ui/components`** — the form renderer maps the entries onto
|
|
544
|
+
`form.setError` and takes over the failure, **but only when every rejected
|
|
545
|
+
field has a visible input to carry it**. If the server also rejected something
|
|
546
|
+
the form does not render, it falls through to the banner, whose top-level
|
|
547
|
+
message concatenates every field's reason — so the part the user cannot see
|
|
548
|
+
inline is still said out loud instead of silently dropped.
|
|
549
|
+
|
|
550
|
+
This also removes the need for the client-side predicate mirroring added in
|
|
551
|
+
#2962: a form no longer has to guess what the server will reject in order to
|
|
552
|
+
warn about it beforehand, and mirrored predicates drift.
|
|
553
|
+
|
|
554
|
+
Non-field failures (403 / permission denials / anything without `fields[]`) take
|
|
555
|
+
exactly the path they took before.
|
|
556
|
+
|
|
557
|
+
- 24e0e0a: feat(components,grid,list): a column-header sort orders the whole list, not the page you can see — #3106
|
|
558
|
+
|
|
559
|
+
Clicking a column header under server pagination sorted **the current page**.
|
|
560
|
+
The user saw "sorted by this column" and got "these fifty rows are in order;
|
|
561
|
+
page 2 starts over". The sort was real — its scope was not the one the screen
|
|
562
|
+
implied — and it had no way out of `data-table` at all: the sort lived in two
|
|
563
|
+
`useState`s with no callback, so the layer that issues the request could not
|
|
564
|
+
see it even in principle.
|
|
565
|
+
|
|
566
|
+
`DataTable` gains `manualSorting` + a controlled `sort` + `onSortChange`. In
|
|
567
|
+
that mode it sorts nothing, reports what a header click asks for, and renders
|
|
568
|
+
`sort` as the indicator — keeping **no** sort state of its own, because a
|
|
569
|
+
private copy beside a controlled prop is the shape the defect had.
|
|
570
|
+
|
|
571
|
+
`ObjectGrid` turns that into a `$orderby` in both of its server modes (its own
|
|
572
|
+
fetch, and a parent-driven one), and `ListView` lands it in `currentSort` — the
|
|
573
|
+
same state the toolbar's sort builder writes. One sort, two controls: that is
|
|
574
|
+
what makes "does a header sort outrank the saved view's sort?" a non-question
|
|
575
|
+
rather than a precedence rule someone has to remember.
|
|
576
|
+
|
|
577
|
+
Three details that are decisions, not incidentals:
|
|
578
|
+
|
|
579
|
+
- **A header click replaces the order** instead of appending to it, so the
|
|
580
|
+
column under the cursor is the one the list is sorted by. Multi-key orders
|
|
581
|
+
still come from the sort builder, and the headers render them numbered.
|
|
582
|
+
- **It cannot ask for "no sort".** In client mode the third click clears, and
|
|
583
|
+
that is meaningful there — the rows return to the order they arrived in.
|
|
584
|
+
Across a server-paged collection there is no such order (objectstack#4363), so
|
|
585
|
+
a header offering it would hand the user a worse lie than the one being fixed.
|
|
586
|
+
Clearing stays with the sort builder, which can restore the view's default.
|
|
587
|
+
- **Relational columns render no sort affordance** under server sorting. A
|
|
588
|
+
`lookup` column shows a related record's name while `$orderby` can only order
|
|
589
|
+
by the stored id (objectstack#4256) — the same reason #3096 removed them from
|
|
590
|
+
the toolbar's sort picker. Client-side sorting keys off the rendered label, so
|
|
591
|
+
those headers stay live there.
|
|
592
|
+
|
|
593
|
+
Client-side tables are untouched: same three-state cycle, same local sort.
|
|
594
|
+
|
|
595
|
+
- 80edbd4: fix(view,components): the spec→FilterBuilder operator table covers the whole view vocabulary, and the dead write direction is gone
|
|
596
|
+
|
|
597
|
+
`view-config-utils`' `SPEC_TO_BUILDER_OP` resolved **10 of the spec's 19
|
|
598
|
+
canonical `VIEW_FILTER_OPERATORS`**. The nine it missed —
|
|
599
|
+
`not_equals`, `starts_with`, `ends_with`, `greater_than`, `less_than`,
|
|
600
|
+
`greater_than_or_equal`, `less_than_or_equal`, `is_null`, `is_not_null` — all
|
|
601
|
+
appear in stored view metadata (they are canonical; `ViewFilterRuleSchema`
|
|
602
|
+
validates against exactly this list), and each reached the FilterBuilder as a
|
|
603
|
+
raw spelling its operator dropdown cannot select.
|
|
604
|
+
|
|
605
|
+
Same defect and same cause as #2974, one table over: spellings were enumerated
|
|
606
|
+
by hand. That table is now derived from the spec's own canonical list and
|
|
607
|
+
`VIEW_FILTER_OPERATOR_ALIASES`, matched case- and separator-insensitively, so
|
|
608
|
+
`not_in` / `notIn` / `'not in'` / `NOT_IN` are one entry rather than four
|
|
609
|
+
chances to miss one.
|
|
610
|
+
|
|
611
|
+
Four canonical operators have no FilterBuilder equivalent —
|
|
612
|
+
`starts_with`/`ends_with` (absent from its vocabulary) and `is_null`/
|
|
613
|
+
`is_not_null` (distinct from the `is_empty`/`is_not_empty` it does have). They
|
|
614
|
+
are recorded as explicit `null`s and asserted, and deliberately left unmapped:
|
|
615
|
+
folding them onto a near-equivalent would silently rewrite the author's
|
|
616
|
+
operator on the next save, whereas an unmapped operator surfaces as a condition
|
|
617
|
+
row the author must complete.
|
|
618
|
+
|
|
619
|
+
Also retired `BUILDER_TO_SPEC_OP` and `toSpecFilter` — the write direction,
|
|
620
|
+
dead since the legacy `buildViewConfigSchema` engine was replaced by the
|
|
621
|
+
studio's spec-driven inspector (no caller anywhere in the repo, and not part of
|
|
622
|
+
`@object-ui/plugin-view`'s public exports). It was objectui's last emitter of
|
|
623
|
+
`'not in'` with a space, plus `before`/`after`, as _filter-AST_ operators —
|
|
624
|
+
spellings that reached the server outside `VALID_AST_OPERATORS` and were dropped
|
|
625
|
+
without an error (objectstack-ai/objectstack#3948).
|
|
626
|
+
|
|
627
|
+
`@object-ui/components` now exports `FILTER_BUILDER_OPERATORS` (and the
|
|
628
|
+
`FilterBuilderOperator` type), derived from the operators the FilterBuilder
|
|
629
|
+
actually renders, so tables mapping onto that vocabulary can assert against it
|
|
630
|
+
instead of restating it.
|
|
631
|
+
|
|
632
|
+
Refs objectstack-ai/objectui#2945, #2901.
|
|
633
|
+
|
|
634
|
+
### Patch Changes
|
|
635
|
+
|
|
636
|
+
- fc0272a: fix(actions): apply the ADR-0066 D4 capability gate on every action surface (framework#3923)
|
|
637
|
+
|
|
638
|
+
An action declaring `requiredPermissions` is supposed to be one declaration with
|
|
639
|
+
two enforcement surfaces: 403 on the server, hidden button in the UI. The UI half
|
|
640
|
+
only ever ran inside `ActionEngine.getActionsForLocation` — and the surfaces
|
|
641
|
+
`record_header`, `record_more`, `list_item` and `list_toolbar` actually render on
|
|
642
|
+
do not go through the engine. They filter their own action lists. So a button
|
|
643
|
+
declaring a capability nobody holds rendered, live and clickable, on the record
|
|
644
|
+
header, in every grid row menu, and on the list toolbar. For a `type: 'api'`
|
|
645
|
+
action pointed at a self-authored endpoint, nothing else was checking either: the
|
|
646
|
+
platform's action route (which is where the 403 comes from) never sees that
|
|
647
|
+
request.
|
|
648
|
+
|
|
649
|
+
`page:header`, `action:bar` (business _and_ `systemActions`) and the grid's
|
|
650
|
+
`RowActionMenu` now apply the same gate, via a shared `useCapabilityGate()` so
|
|
651
|
+
the surfaces cannot drift apart. The rule is the engine's, unchanged: hide unless
|
|
652
|
+
the caller holds **all** declared capabilities; an empty held set is "holds
|
|
653
|
+
nothing" and gates; **unknown** — no action runtime, no resolved capabilities —
|
|
654
|
+
fails OPEN, because the server is the authority and hiding a permitted user's
|
|
655
|
+
button on missing client data is the worse failure.
|
|
656
|
+
|
|
657
|
+
The record surface was also feeding the gate nothing to work with.
|
|
658
|
+
`RecordDetailView` mounts its own `<ActionProvider>`, which shadows the shell's
|
|
659
|
+
for every action on that page, and seeded it with identity only — no
|
|
660
|
+
`systemPermissions`. Since unknown fails open, that alone un-gated every
|
|
661
|
+
`record_header` / `record_more` / `record_section` action on the one page those
|
|
662
|
+
locations exist on. It now forwards the caller's resolved capabilities (and only
|
|
663
|
+
once they have actually resolved, so a standalone embed without a
|
|
664
|
+
`PermissionProvider` keeps failing open rather than hiding everything).
|
|
665
|
+
|
|
666
|
+
`useRecordEditable`'s record-level explain probe went out on a bare
|
|
667
|
+
`fetch(..., { credentials: 'include' })`. A bearer-token session carries its
|
|
668
|
+
credential in the `Authorization` header, not a cookie, so the probe came back
|
|
669
|
+
401 on a perfectly valid admin session and the verdict silently failed open —
|
|
670
|
+
the hook was inert in exactly the deployments it was written for. It now rides
|
|
671
|
+
the host's authenticated fetch (`SchemaRendererProvider`'s `apiFetch`), falling
|
|
672
|
+
back to the global one for standalone embeds.
|
|
673
|
+
|
|
674
|
+
- c785740: fix(detail): record Attachments become their own tab (with count badge) and their copy is translated — objectstack#4358
|
|
675
|
+
|
|
676
|
+
Two defects on `enable.files: true` record detail pages:
|
|
677
|
+
|
|
678
|
+
1. **Buried placement.** `RecordDetailView` appended `RecordAttachmentsPanel`
|
|
679
|
+
AFTER the schema-rendered page tree, whose synthesized default embeds
|
|
680
|
+
`record:discussion` as the last main component — so the panel always
|
|
681
|
+
landed below an ever-growing feed timeline, undiscoverable without
|
|
682
|
+
scrolling to the very bottom, with no metadata knob to move it.
|
|
683
|
+
|
|
684
|
+
`buildDefaultTabs` now emits a peer **Attachments** tab (a new
|
|
685
|
+
`record:attachments` node rendered by an app-shell registration wrapping
|
|
686
|
+
the existing panel via RecordContext) between Related and
|
|
687
|
+
Activity/History. `PageTabsRenderer` derives the tab's count badge from a
|
|
688
|
+
`sys_attachment` probe scoped to `(parent_object, parent_id)`, riding the
|
|
689
|
+
same RelatedCountStore cache/invalidation bus as related-list badges — so
|
|
690
|
+
uploads and deletes update the badge live. A `hideAttachments` synthesizer
|
|
691
|
+
option suppresses the tab; RecordDetailView keeps its legacy bottom append
|
|
692
|
+
only as the fallback for authored pages without the node
|
|
693
|
+
(`hasExplicitAttachments`).
|
|
694
|
+
|
|
695
|
+
2. **Untranslated copy.** The panel's eleven `detail.*` keys (`attachments`,
|
|
696
|
+
`uploadAttachment`, `loadingAttachments`, `noAttachments`,
|
|
697
|
+
`downloadAttachment`, `deleteAttachment`, and the five
|
|
698
|
+
`attachment*Denied/Required` friendly errors) existed only as inline
|
|
699
|
+
English `defaultValue`s — no locale bundle carried them, so non-English
|
|
700
|
+
consoles always showed English. All ten locales now define them; the tab
|
|
701
|
+
label rides the existing well-known-label dictionary (→ 附件 etc.).
|
|
702
|
+
|
|
703
|
+
- b41f401: **Authoring types are input types (framework#4074 steps 2–3): `ActionParam` takes the spec's declaration forms, `ListViewSchema` stops promising parse-output defaults, and `FormField.dependsOn` matches its runtime reader.**
|
|
704
|
+
|
|
705
|
+
Three public types said something different from what the platform accepts. All
|
|
706
|
+
three divergences were found by making `packages/types`' tests compile (#3009)
|
|
707
|
+
and then resolving the declared `p1-spec-alignment.test.ts` debt site-by-site
|
|
708
|
+
instead of papering over it.
|
|
709
|
+
|
|
710
|
+
**`ActionParam` is now the authoring shape, aligned with the spec's input.**
|
|
711
|
+
`name` / `label` / `type` become optional and `field` / `objectOverride` appear:
|
|
712
|
+
the spec's primary way to declare a param — a bare field reference that inherits
|
|
713
|
+
label/type/validation/options from an object field — was unrepresentable while
|
|
714
|
+
all three were required. The _resolved_ shape the dialog consumes (after
|
|
715
|
+
app-shell's `resolveActionParams()` inlines the reference) remains
|
|
716
|
+
`@object-ui/core`'s `ActionParamDef`, with all three required. Authoring and
|
|
717
|
+
resolved are different types on purpose. `label` and option labels take the
|
|
718
|
+
spec's `I18nLabel` by import — which the new compile-time guard promptly
|
|
719
|
+
revealed to be aliased to plain `string` in the current spec (the per-locale
|
|
720
|
+
record is the separate `I18nObject`), so this is not a behavioural widening
|
|
721
|
+
today; importing the alias means objectui tracks any future widening
|
|
722
|
+
automatically.
|
|
723
|
+
|
|
724
|
+
**Breaking:** code destructuring `param.name` / `param.label` / `param.type` as
|
|
725
|
+
guaranteed must now handle the field-backed form (or consume the resolved
|
|
726
|
+
`ActionParamDef` instead, which is what dialog-side code should be doing).
|
|
727
|
+
|
|
728
|
+
**`ListViewInferred` is `z.input`, not `z.infer`.** The spec sub-schemas that
|
|
729
|
+
flow into the list-view surface (`userActions`, `tabs` → `ViewTab`, `sharing`)
|
|
730
|
+
carry `.default()`s, so the inferred output type made fields like
|
|
731
|
+
`userActions.refresh` or a tab's `pinned`/`visible` _required_ — but nothing on
|
|
732
|
+
the render path ever runs `.parse()`: `normalizeListViewSchema` deliberately
|
|
733
|
+
applies no defaults ("an absent flag stays absent", its own suite). The output
|
|
734
|
+
type therefore rejected valid authored metadata (`userActions: { sort: true }`)
|
|
735
|
+
while promising renderers defaults that never arrive. Typing the surface as
|
|
736
|
+
input matches both the author and the runtime object. Code that _trusted_ those
|
|
737
|
+
phantom defaults now gets an optionality error — which is a latent bug surfacing,
|
|
738
|
+
not a regression: the value really could be absent.
|
|
739
|
+
|
|
740
|
+
**`FormField.dependsOn` is `DependsOnInput`.** The runtime reader
|
|
741
|
+
(`resolveCascadingOptions`) has always accepted a bare name, a list of names, or
|
|
742
|
+
lookup-parameter entries `{ field, param }` — its parameter type says so. The
|
|
743
|
+
public property said `string`, so array-authored metadata type-errored while
|
|
744
|
+
working, and the form renderer read the key through `(f as any).dependsOn` to
|
|
745
|
+
get past its own type. The shape now lives in `@object-ui/types` (single source
|
|
746
|
+
of truth next to `FormField`), `@object-ui/core` imports and re-exports it, and
|
|
747
|
+
the two `as any` reads in the components form renderer are typed.
|
|
748
|
+
|
|
749
|
+
**The `p1-spec-alignment.test.ts` exclusion is gone.** Its 14 errors resolved:
|
|
750
|
+
the two "sharing in ObjectUI format" tests and the legacy-ARIA-spelling fixture
|
|
751
|
+
are deleted/rewritten — those dialects are _normalizer input_, folded by
|
|
752
|
+
`normalizeListViewSchema` and asserted branch-by-branch in core's
|
|
753
|
+
`normalize-list-view.test.ts`, the seam where the fold actually runs; asserting
|
|
754
|
+
them on the canonical type only ever "passed" because nothing compiled the file.
|
|
755
|
+
One fixture claimed a shape no surface ever admitted (an ObjectQL triplet as a
|
|
756
|
+
spec `ViewTab.filter`) and was corrected to the rule-object form. Every test
|
|
757
|
+
file in `@object-ui/types` is now compiled, with no exclusions.
|
|
758
|
+
|
|
759
|
+
Discrimination-checked: reverting `ListViewInferred` to `z.infer`, `dependsOn`
|
|
760
|
+
to `string`, or `ActionParam.name` to required each produces the expected
|
|
761
|
+
compile error in the now-compiled test files (`TS2739` / `TS2322` / `TS2741`);
|
|
762
|
+
restored, all projects are clean.
|
|
763
|
+
|
|
764
|
+
- 68ef584: fix(test-setup): stop shadowing ten real registrations, and declare page:header's inputs
|
|
765
|
+
|
|
766
|
+
`vitest.setup.dom.tsx` re-registered `text`, `email`, `password`, `textarea`,
|
|
767
|
+
`image`, `html`, `avatar`, `select`, `slider` and `grid` by hand — ~380 lines of
|
|
768
|
+
renderer copied out of @object-ui/components — to undo bare-name fallbacks that
|
|
769
|
+
@object-ui/fields and the plugins claimed by loading after it.
|
|
770
|
+
|
|
771
|
+
Both sides now register under their own namespace with `skipFallback: true`, so
|
|
772
|
+
nothing overwrites the `ui:` originals and the workaround is obsolete. It was
|
|
773
|
+
not free: the copies carried no `inputs` and no `defaultProps`, so inside the
|
|
774
|
+
test environment four curated public blocks reported an empty configuration
|
|
775
|
+
surface while the real registrations declare one. `apps/console`'s contract
|
|
776
|
+
test reads that registry, so its picture of the contract was fiction for those
|
|
777
|
+
tags — a guard that measures a fixture instead of the product.
|
|
778
|
+
|
|
779
|
+
Deleting the block restores what the app actually boots with. Verified: the ten
|
|
780
|
+
tags keep their namespace and canonical type, and their declared surface comes
|
|
781
|
+
back — `text` 1 input, `email` 6, `password` 6, `textarea` 6, `image` 3,
|
|
782
|
+
`html` 1, `avatar` 4, `select` 6, `slider` 5, `grid` 7, plus `defaultProps`.
|
|
783
|
+
The heavy DOM setup also got roughly twice as fast (~545s to ~235s of setup
|
|
784
|
+
time across the suite), since every file in that project was paying to evaluate
|
|
785
|
+
the duplicated renderers.
|
|
786
|
+
|
|
787
|
+
With the shadowing gone, `page:header` was left as a genuine gap: a curated
|
|
788
|
+
public block whose renderer reads `title`, `subtitle`, `actions`, `breadcrumb`,
|
|
789
|
+
`recordChrome`, `showStar` and `showCopyId`, with none of them declared. Now
|
|
790
|
+
declared.
|
|
791
|
+
|
|
792
|
+
`element:divider` keeps zero inputs on purpose — its renderer reads only
|
|
793
|
+
`className`, so there is nothing to author.
|
|
794
|
+
|
|
795
|
+
- c769d3d: fix(form): a `defaultValues` change no longer discards the field the user is filling
|
|
796
|
+
|
|
797
|
+
The form renderer adopts a changed `defaultValues` with `form.reset()`, which
|
|
798
|
+
replaces the **whole** react-hook-form record — so it also blanks the fields the
|
|
799
|
+
incoming defaults say nothing about. And it runs in a **passive effect**, one
|
|
800
|
+
commit after those fields have been committed and painted, so input landing in
|
|
801
|
+
that window was silently dropped.
|
|
802
|
+
|
|
803
|
+
The caught case is the wizard (objectui#2982). It reuses ONE inner form across
|
|
804
|
+
steps and feeds it `defaultValues={formData}` — the merge of the steps submitted
|
|
805
|
+
**so far** — so at every step boundary the incoming defaults are missing exactly
|
|
806
|
+
the fields now on screen:
|
|
807
|
+
|
|
808
|
+
```
|
|
809
|
+
RESET to {"name":"Alice"} (values before: {"name":"Alice","note":"hello"})
|
|
810
|
+
-> create POST {"name":"Alice"} — the last step is gone
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
In a browser this needs a busy main thread plus typing on the first frame after
|
|
814
|
+
the new defaults arrive — unlikely by hand, but paste and autofill land in a
|
|
815
|
+
single tick. The same shape had already bitten once before, as a `reset()` on
|
|
816
|
+
`defaultValues` **identity** churn wiping input mid-interaction; comparing by
|
|
817
|
+
value fixed that, and this is the residual hole where the value genuinely did
|
|
818
|
+
change.
|
|
819
|
+
|
|
820
|
+
The reset now carries such a value across instead of dropping it. Deliberately
|
|
821
|
+
narrow: only a field the **caller has never carried** — absent from both the
|
|
822
|
+
outgoing and the incoming defaults — and whose value the user actually changed
|
|
823
|
+
is eligible. Wherever the caller has an opinion it stays authoritative, so the
|
|
824
|
+
load-bearing paths are unchanged:
|
|
825
|
+
|
|
826
|
+
- an edit-mode record landing after first paint still fills every field it names
|
|
827
|
+
(a field the user has NOT touched is empty-ish against the baseline, using the
|
|
828
|
+
same comparison the dirty check uses, so a widget normalizing its own empty
|
|
829
|
+
value on mount is not mistaken for input);
|
|
830
|
+
- a `recordId` swap still replaces the record outright — drawer/modal/split
|
|
831
|
+
forms re-fetch without re-entering their loading branch, so record B lands in
|
|
832
|
+
the still-mounted form and must not inherit an abandoned edit to record A;
|
|
833
|
+
- a field the caller withdraws from its defaults stops being the user's.
|
|
834
|
+
|
|
835
|
+
A reset that carries input now also reports the form as dirty (it is, against
|
|
836
|
+
the caller's defaults) instead of unconditionally announcing pristine, so a
|
|
837
|
+
host's discard guard keeps hearing the truth.
|
|
838
|
+
|
|
839
|
+
- 94e63ef: fix(form): the runtime `field` metadata slot is declared instead of smuggled, and importing the spec's FormField is a lint error — #3090
|
|
840
|
+
|
|
841
|
+
`FormField.field` — the slot where object-bound form paths stash the resolved
|
|
842
|
+
field-metadata **object** for widgets — rode through the index signature,
|
|
843
|
+
undeclared, readable only via `as any`. Same key, different layer: in the spec
|
|
844
|
+
form-view vocabulary `field` is a _string_ (the referenced object-field name),
|
|
845
|
+
and the undeclared slot kept that pun latent. The slot is now declared
|
|
846
|
+
(`field?: Record<string, any>`) with the invariant in its JSDoc: on a runtime
|
|
847
|
+
FormField it is never a string — the authored string form ends at the
|
|
848
|
+
`normalizeSectionField` chokepoint, and a tripwire test pins that across all
|
|
849
|
+
three input shapes. Assigning a string is now a compile error; the `as any`
|
|
850
|
+
casts at the read sites are gone.
|
|
851
|
+
|
|
852
|
+
A `no-restricted-imports` tripwire bans importing `FormField`/
|
|
853
|
+
`FormFieldSchema` from `@objectstack/spec/ui` inside this repo: the spec's
|
|
854
|
+
FormField TYPE erases to `any` in its dist (objectstack#4171), so the
|
|
855
|
+
misimport silently deletes type safety — tsc says nothing. The lint message
|
|
856
|
+
names the two layers and the correct import. The drift-guard parity test is
|
|
857
|
+
the one legitimate importer, exempted inline with its reason.
|
|
858
|
+
|
|
859
|
+
Ledger: `FormField` and `FormFieldSchema` move from untriaged DEBT to ALLOW
|
|
860
|
+
with the two-layer rationale written down (122 → 120).
|
|
861
|
+
|
|
862
|
+
- c735bf7: fix(form): a spec-vocabulary field no longer crashes the standalone form, and every surface now says which vocabulary you meant — #3090
|
|
863
|
+
|
|
864
|
+
Writing the regression test against the unfixed renderer proved the failure
|
|
865
|
+
was worse than the assumed silent drop: a `{ field: 'x' }` entry (spec
|
|
866
|
+
form-VIEW vocabulary) slipped past the `f?.name` guards into a
|
|
867
|
+
react-hook-form Controller with `name === undefined` and crashed the whole
|
|
868
|
+
standalone form on `name.split('.')`, with nothing naming the culprit entry.
|
|
869
|
+
The renderer now partitions such entries out — the rest of the form renders —
|
|
870
|
+
and surfaces them with an inline alert plus a console.error whose text is the
|
|
871
|
+
fix instruction (rename to `name`, or use an object-bound form whose sections
|
|
872
|
+
accept the spec shape).
|
|
873
|
+
|
|
874
|
+
`objectui validate` grows the same boundary awareness: on failure, a
|
|
875
|
+
`{ field: … }` entry in a standalone form gets a "likely cause" hint naming
|
|
876
|
+
the real fix instead of the bare `invalid_union` — the previous message read
|
|
877
|
+
as "bolt a `name` on", which converts spec metadata wrongly. On success,
|
|
878
|
+
mixed-vocabulary entries (`name` + string `field`) get a warning: they
|
|
879
|
+
validate, but the spec key is dead weight the renderer ignores.
|
|
880
|
+
|
|
881
|
+
`normalizeSectionField` warns (once per site) when an authored section field
|
|
882
|
+
mixes both identity keys — the spec branch derives the runtime name from
|
|
883
|
+
`field`, so an authored `name` was silently overwritten.
|
|
884
|
+
|
|
885
|
+
- 02aef0c: fix(sdui): a `kind:'html'` page can use lazily-registered blocks, and recovers when one registers late
|
|
886
|
+
|
|
887
|
+
objectui#2953 had a twin one tier over, unreported. The whitelist a
|
|
888
|
+
`kind:'html'` page's source compiles against was built from `getAllTypes()` +
|
|
889
|
+
`getConfig()` — both loaded-only — so any block registered via `registerLazy()`
|
|
890
|
+
was rejected as _"not an allowed component"_.
|
|
891
|
+
|
|
892
|
+
The blast radius is worse than the react tier's. There, a missing block cost one
|
|
893
|
+
identifier; here a compile diagnostic fails the **whole page**, so a single
|
|
894
|
+
`<object-kanban>` replaced the entire page with `HTML page failed to compile (2)`.
|
|
895
|
+
And it never recovered: `layoutElement` was memoised on `[schema, pageType]` with
|
|
896
|
+
no registry signal, so the cached error panel outlived the plugin actually
|
|
897
|
+
landing — permanently broken for the session.
|
|
898
|
+
|
|
899
|
+
`ComponentRegistry` gains three lazy-aware reads:
|
|
900
|
+
|
|
901
|
+
- `getKnownTypes()` — loaded registrations **plus** pending lazy stubs, deduped.
|
|
902
|
+
The set a whitelist or manifest should be built from. `getAllTypes()` keeps its
|
|
903
|
+
loaded-only meaning ("what can render right now") and now says so.
|
|
904
|
+
- `getMeta(type, namespace?)` — metadata from the loaded registration, else from
|
|
905
|
+
a pending stub. `getConfig()` stays loaded-only, since callers read
|
|
906
|
+
`.component` off it.
|
|
907
|
+
- `getVersion()` — monotonic counter of changes to the known set, bumped on
|
|
908
|
+
register / unregister / registerLazy. A cache key that a type _count_ cannot
|
|
909
|
+
substitute for: one registration plus one unregistration leaves the count
|
|
910
|
+
untouched while the set changed.
|
|
911
|
+
|
|
912
|
+
`getJsxManifest()` builds from those, and `PageRenderer` subscribes to the
|
|
913
|
+
registry so a page that could not compile retries when the registry grows.
|
|
914
|
+
|
|
915
|
+
A stub carries no `inputs` yet, so its props surface as `unknown-prop` warnings
|
|
916
|
+
rather than errors — the page compiles and renders, and the inner
|
|
917
|
+
`SchemaRenderer` triggers the loader and swaps in the real block. Authoring-time
|
|
918
|
+
prop validation is unaffected: `sdui.manifest.json` is generated with every
|
|
919
|
+
plugin eagerly loaded, and asserts as much.
|
|
920
|
+
|
|
921
|
+
- 9a04d25: fix(registry): prefix every namespaced key exactly once, in every namespace
|
|
922
|
+
|
|
923
|
+
objectui#3023 fixed eleven `record:*` blocks registered as
|
|
924
|
+
`register('record:x', …, { namespace: 'record' })` — an already-prefixed name
|
|
925
|
+
handed to a registry that prefixes it again, landing the block at
|
|
926
|
+
`record:record:x` — and guarded that namespace alone. Twenty-two more were
|
|
927
|
+
sitting in `action:` (5), `element:` (10) and `page:` (7), two of them
|
|
928
|
+
(`page:header`, `element:divider`) curated public blocks.
|
|
929
|
+
|
|
930
|
+
Checking one namespace is exactly what let them keep sitting there, so the
|
|
931
|
+
guard now asks the whole registry rather than a prefix of it.
|
|
932
|
+
|
|
933
|
+
Same fix as before: register the bare name and let `namespace` do the
|
|
934
|
+
prefixing, with `skipFallback: true` so the fallback does not claim that bare
|
|
935
|
+
name globally. It would otherwise take over `header`, `footer`, `sidebar`,
|
|
936
|
+
`tabs`, `card`, `accordion`, `section`, `text`, `image`, `button`, `icon` —
|
|
937
|
+
every one of which belongs to `ui:`. All 22 stay reachable exactly as
|
|
938
|
+
`<namespace>:<name>`; the registry goes 522 keys to 500, and the contract is
|
|
939
|
+
unchanged at 42/42.
|
|
940
|
+
|
|
941
|
+
Found while probing why six curated Tier B primitives report no `inputs`. They
|
|
942
|
+
do declare them — `vitest.setup.dom.tsx` registers simplified `text` / `image` /
|
|
943
|
+
`html` / `grid` stubs that shadow the real registrations inside the test
|
|
944
|
+
environment only. That shadowing is a separate question, left alone here; the
|
|
945
|
+
doubled keys it turned up are not test-environment artifacts.
|
|
946
|
+
|
|
947
|
+
- eb4b740: feat(page,element): declare inputs for the eight configurable page:\* / element:\* blocks
|
|
948
|
+
|
|
949
|
+
Same gap objectui#3027 closed for `record:*`, in the two namespaces next door:
|
|
950
|
+
renderers that read real props while every authoring surface reported "takes no
|
|
951
|
+
configuration". Declarations mirror what each renderer reads —
|
|
952
|
+
|
|
953
|
+
| block | inputs |
|
|
954
|
+
| ------------------------- | -------------------------------------------------------------------------------- |
|
|
955
|
+
| `page:tabs` | items, tabStyle (line/card/pill), position (top/left) |
|
|
956
|
+
| `page:card` | title, bordered, body (slot), footer (slot) |
|
|
957
|
+
| `page:accordion` | items, allowMultiple, variant (flush/card) |
|
|
958
|
+
| `element:text` | content, variant (heading/subheading/body/caption), align |
|
|
959
|
+
| `element:number` | object, aggregate (count/sum/avg/min/max), field, filter, format, prefix, suffix |
|
|
960
|
+
| `element:button` | label, action (inline ActionDef), variant, size, icon, iconPosition, disabled |
|
|
961
|
+
| `element:definition-list` | items, columns, inline |
|
|
962
|
+
| `element:repeater` | object, titleField, fields, filter, sort, limit, emptyText, divided |
|
|
963
|
+
|
|
964
|
+
`page:section`, `page:footer` and `page:sidebar` are left at zero inputs on
|
|
965
|
+
purpose: their renderers render `children`/`body` and nothing else — like
|
|
966
|
+
`element:divider`, they are genuinely prop-less containers, and inventing
|
|
967
|
+
inputs for them would be the opposite falsehood.
|
|
968
|
+
|
|
969
|
+
`aria` and `className` stay undeclared throughout, per the convention on
|
|
970
|
+
`record:details`: escape hatches and styling pass-throughs, not authoring
|
|
971
|
+
choices. `element:button`'s registration also documents the split against
|
|
972
|
+
`action:button` — inline ActionDef vs a declared action referenced by name —
|
|
973
|
+
so the two stop reading as duplicates.
|
|
974
|
+
|
|
975
|
+
Declaration only; no renderer behavior changes. Curation (which of these join
|
|
976
|
+
`PUBLIC_BLOCKS`) is a separate change.
|
|
977
|
+
|
|
978
|
+
- 5b084eb: fix(sdui): the react page's "no adapter yet" fallback stops churning its provider context
|
|
979
|
+
|
|
980
|
+
Audit of the remaining half of `ReactKindPage`'s scope memo, `[schema, adapter]`.
|
|
981
|
+
The `schema` half was the live bug fixed in objectui#2984; this is the adapter
|
|
982
|
+
half.
|
|
983
|
+
|
|
984
|
+
**The hosts are fine.** Both `AdapterCtx.Provider` call sites pass a stable
|
|
985
|
+
value — `AdapterProvider` from `useState`, the console preview from a module
|
|
986
|
+
constant — so there is no state loss in the shipped app.
|
|
987
|
+
|
|
988
|
+
**One real instance remained**, one layer down: `<SchemaRendererProvider
|
|
989
|
+
dataSource={adapter ?? {}}>` minted a fresh object on every render while the
|
|
990
|
+
adapter was still null (the window before the host connects). That is a context
|
|
991
|
+
value, and `SchemaRendererProvider` memoises on its identity, so every block
|
|
992
|
+
inside the page had its schema re-cloned and its expressions re-run on each
|
|
993
|
+
render of the page. Now a module constant, like the `SchemaRenderer` fallback
|
|
994
|
+
it mirrors.
|
|
995
|
+
|
|
996
|
+
**The `adapter` dependency itself must stay**, and is now pinned. It looks like
|
|
997
|
+
the obvious thing to optimise away — it is the last remaining trigger that can
|
|
998
|
+
recompile a page and cost its `useState`. But `ReactRunner` hands React the same
|
|
999
|
+
element object while `(code, scope)` hold, and React bails out on an identical
|
|
1000
|
+
element reference, so the page subtree never re-renders on its own: recompiling
|
|
1001
|
+
is the _only_ path by which a new adapter reaches the blocks inside the page.
|
|
1002
|
+
Removing the dependency strands every block on the first adapter forever — no
|
|
1003
|
+
error, just a dead data source. `react-page-adapter.test.tsx` pins both
|
|
1004
|
+
directions, so the tradeoff cannot be quietly re-litigated.
|
|
1005
|
+
|
|
1006
|
+
Docs: the react-pages guide now states the host-side requirement — an adapter
|
|
1007
|
+
constructed inline on every render resets every react page on every render.
|
|
1008
|
+
|
|
1009
|
+
- aa1240a: fix(sdui): lazily-registered public blocks reach a `kind:'react'` page's scope, and ReactRunner keeps the errors it catches
|
|
1010
|
+
|
|
1011
|
+
Two defects in the trusted `kind:'react'` page tier.
|
|
1012
|
+
|
|
1013
|
+
**objectui#2953 — the contract skipped lazy blocks.** `getPublicConfigs()`
|
|
1014
|
+
resolved every curated `PUBLIC_BLOCKS` tag through `getConfig()`, which reads
|
|
1015
|
+
loaded registrations only, so a block registered with `registerLazy()` was
|
|
1016
|
+
absent from the contract until its plugin chunk happened to be imported. In
|
|
1017
|
+
`apps/console` that silently dropped `object-kanban`, `object-calendar`,
|
|
1018
|
+
`object-gantt`, `object-timeline`, `object-map` and `markdown` from every react
|
|
1019
|
+
page's scope — writing `<ObjectKanban/>` threw `ReferenceError` even though the
|
|
1020
|
+
tag is a first-class contract member, and whether it threw depended on load
|
|
1021
|
+
order. `getPublicConfigs()` now resolves pending lazy stubs too, returning them
|
|
1022
|
+
with `lazy: true` and no `component` (new `PublicComponentConfig` type); the
|
|
1023
|
+
injected wrapper renders through `SchemaRenderer`, which triggers the loader and
|
|
1024
|
+
shows its placeholder. `getConfig()` stays loaded-only by design.
|
|
1025
|
+
|
|
1026
|
+
**objectui#2954 — ReactRunner discarded its own error state.**
|
|
1027
|
+
`getDerivedStateFromProps` re-transpiled and re-evaluated the page source on
|
|
1028
|
+
every render and unconditionally set `error: null`. React runs it before the
|
|
1029
|
+
re-render that follows `getDerivedStateFromError`, so the boundary threw away
|
|
1030
|
+
the error it had just caught, rebuilt an identical throwing element, and the
|
|
1031
|
+
throw escaped past its own `fallback` to the renderer's generic panel; `onError`
|
|
1032
|
+
was gated on state that had already been cleared and never fired for a
|
|
1033
|
+
compile-time error at all; and each compile minted a fresh page function — a new
|
|
1034
|
+
element type — that remounted the subtree and wiped the page's `useState`. The
|
|
1035
|
+
transpile+eval is now memoised on `(code, scope)`, errors persist until the
|
|
1036
|
+
inputs actually change, and `onError` reports each error exactly once.
|
|
1037
|
+
|
|
1038
|
+
- 2374a49: fix(sdui): a react page no longer loses its state to a memo that never held, and a source that exports nothing fails loudly
|
|
1039
|
+
|
|
1040
|
+
Writing the regression guard for objectui#2954's "latent hazard" found it was
|
|
1041
|
+
already real.
|
|
1042
|
+
|
|
1043
|
+
**`evaluatedSchema` was memoised on values rebuilt every render.**
|
|
1044
|
+
`SchemaRenderer` fell back to a fresh `{}` when no `SchemaRendererProvider` sat
|
|
1045
|
+
above it, and `usePageVariables()` returned a brand-new object literal outside a
|
|
1046
|
+
`PageVariablesProvider`. Both feed the `evaluatedSchema` memo's dependency list,
|
|
1047
|
+
so for any tree without those providers the memo never hit: the schema was
|
|
1048
|
+
re-cloned and the ExpressionEvaluator re-run on every render, and children got a
|
|
1049
|
+
new schema identity every time. A `kind:'react'` page memoises its compiled
|
|
1050
|
+
source on that identity, so the page was recompiled — a new page function, a new
|
|
1051
|
+
element type — and React remounted it, silently discarding the user's `useState`.
|
|
1052
|
+
Any registry notification (every lazy plugin's first load) triggered it. Both
|
|
1053
|
+
fallbacks are now module constants.
|
|
1054
|
+
|
|
1055
|
+
**A source that exports nothing now throws instead of rendering blank.**
|
|
1056
|
+
`generateElement` inserts the implicit `export default` only when the source
|
|
1057
|
+
_starts with_ JSX, a `function` declaration, `()` or `class` — so the very
|
|
1058
|
+
common `const Page = () => …` exported nothing, and the page rendered blank with
|
|
1059
|
+
no error reported anywhere. It now throws with a message naming the fix, which
|
|
1060
|
+
`ReactRunner`'s error panel surfaces. `export default null` still means "render
|
|
1061
|
+
nothing"; a default export that is not a component throws too.
|
|
1062
|
+
|
|
1063
|
+
**`PageSchema['kind']` matches `@objectstack/spec`.** It declared
|
|
1064
|
+
`'full' | 'slotted'` while the renderer had shipped `'react'` and
|
|
1065
|
+
`'html'`/`'jsx'` since ADR-0080 and read the field through a cast. The union now
|
|
1066
|
+
spells all five and the cast is gone.
|
|
1067
|
+
|
|
1068
|
+
Docs: new `content/docs/guide/react-pages.md` (choosing between the executed and
|
|
1069
|
+
parsed tiers, the capability gate, the injected scope, flat props, `Block`,
|
|
1070
|
+
`useAdapter`, source shapes, error handling) and a `@object-ui/react-runtime`
|
|
1071
|
+
README — the package had neither, while being the tier AI-authored pages target.
|
|
1072
|
+
|
|
1073
|
+
- 2d5d594: fix(list,detail): sorting a lookup column no longer orders by an invisible key — #3096
|
|
1074
|
+
|
|
1075
|
+
A relational column (`lookup` / `master_detail` / `user` / `tree`) never holds
|
|
1076
|
+
the string its cell shows: it holds the `$expand`-ed record, or a raw foreign-key
|
|
1077
|
+
id whose label was resolved separately. Every sort path took that raw value as
|
|
1078
|
+
its key, so the column of names came back in an order with no relation to the
|
|
1079
|
+
names — sorting looked broken, with nothing saying the key was something else.
|
|
1080
|
+
|
|
1081
|
+
The two halves are fixed differently, because they can order by different things:
|
|
1082
|
+
|
|
1083
|
+
- **Client-side sorts** (grid column headers, any `data-table`, a non-windowed
|
|
1084
|
+
related list) now key off the label the cell renders, via the new
|
|
1085
|
+
`getSortValue` / `compareSortValues` in `@object-ui/core` — which resolves an
|
|
1086
|
+
expanded record through `getRecordDisplayName` (ADR-0079), so the sort key and
|
|
1087
|
+
the lookup cell agree on which field names a record. This replaces two broken
|
|
1088
|
+
comparators: `a[col] < b[col]` is always false between two objects (the
|
|
1089
|
+
comparator collapsed to a constant and permuted the rows), and
|
|
1090
|
+
`String(a[col])` is `"[object Object]"` (every row compared equal, so the sort
|
|
1091
|
+
silently did nothing).
|
|
1092
|
+
- **Server `$orderby` sorts** cannot be fixed here — the key is the stored id by
|
|
1093
|
+
construction, and `objectstack#4256` settled that no relation join is coming.
|
|
1094
|
+
So those entry points stop offering the illusion: the ListView toolbar sort
|
|
1095
|
+
picker withholds relational fields and explains why (pointing at a formula
|
|
1096
|
+
field as the supported way to sort by a related name), and a windowed related
|
|
1097
|
+
list renders no sort button for them.
|
|
1098
|
+
|
|
1099
|
+
A relational field the view's CURRENT sort already uses stays listed, labelled
|
|
1100
|
+
`(by ID)`, so view metadata authored or saved with such a sort round-trips
|
|
1101
|
+
instead of rendering a blank row and losing the sort on the next edit.
|
|
1102
|
+
|
|
1103
|
+
- 379728f: fix(fields): a `select` no longer wipes itself when its value outruns its options (#2968)
|
|
1104
|
+
|
|
1105
|
+
Radix keeps a hidden native `<select>` mirror so a Select's value takes part in
|
|
1106
|
+
native form submission. Assigning a value that mirror has no `<option>` for is a
|
|
1107
|
+
no-op — the element stays on `''` — but Radix still dispatches the synthetic
|
|
1108
|
+
`change`, so `''` comes straight back out through `onValueChange` and lands in
|
|
1109
|
+
react-hook-form on top of the value the caller just set.
|
|
1110
|
+
|
|
1111
|
+
The window is not theoretical: `SelectContent` registers its native options a
|
|
1112
|
+
commit AFTER the trigger mounts, so a record that lands after first paint — an
|
|
1113
|
+
edit modal whose `findOne` is still in flight — resets the form into exactly
|
|
1114
|
+
that gap. Every rendered select came back empty while RHF's `_defaultValues`
|
|
1115
|
+
still held the right value. When one of the wiped fields is the one a
|
|
1116
|
+
`visibleWhen` predicate reads, the predicate flips back to false, the
|
|
1117
|
+
conditional fields hide again and the form **latches** in the broken state:
|
|
1118
|
+
pressing Update then fails validation, or submits an empty enum, on a form the
|
|
1119
|
+
user never touched. The wipe is also recorded as a user edit, so Cancel prompts
|
|
1120
|
+
"discard changes?" on an untouched form.
|
|
1121
|
+
|
|
1122
|
+
`SelectItem` rejects `value=""` outright, so `''` can never be a value the user
|
|
1123
|
+
actually picked — it is always the mirror talking. It is now dropped at the
|
|
1124
|
+
single `Select` chokepoint, which covers every surface that renders one (object
|
|
1125
|
+
form, inline grid editor, action param dialog). Clearing a select still goes
|
|
1126
|
+
through `undefined`, which is untouched — the `dependsOn` cascade-clear behaves
|
|
1127
|
+
exactly as before.
|
|
1128
|
+
|
|
1129
|
+
- 7f23cd0: fix(form): a numeric/boolean select option survives selection with its type intact — #3090
|
|
1130
|
+
|
|
1131
|
+
`SelectOptionSchema.value` has accepted `string | number | boolean` for as
|
|
1132
|
+
long as it has existed, but the Radix controls underneath speak strings:
|
|
1133
|
+
picking `{ value: 2 }` silently submitted `"2"` — a wrong-typed write into a
|
|
1134
|
+
number field that nothing on the client ever reported. (Display half-worked:
|
|
1135
|
+
a numeric default matched its numeric item; only SELECTION morphed the type.)
|
|
1136
|
+
|
|
1137
|
+
The renderers now stringify on the way into the control and map the selection
|
|
1138
|
+
back to the AUTHORED option value on the way out (`matchOptionValue`), across
|
|
1139
|
+
the in-form select, the standalone `type: 'select'` component, and the
|
|
1140
|
+
standalone `type: 'radio-group'` component. The TS types stop lying to match:
|
|
1141
|
+
`SelectOption.value` / `RadioOption.value` and the corresponding
|
|
1142
|
+
`value`/`defaultValue`/`onChange` channels widen to what the zod schemas
|
|
1143
|
+
always accepted — a call site treating `option.value` as `string` is now a
|
|
1144
|
+
compile error pointing at a real latent crash, not a false comfort.
|
|
1145
|
+
|
|
1146
|
+
The ripple the widening named, handled at each boundary: `@object-ui/core`'s
|
|
1147
|
+
`OptionLike.value` widens (the option engines compare by identity, so values
|
|
1148
|
+
flow opaquely; the option-lint's CEL-literal domain stringifies at its
|
|
1149
|
+
boundary), and the multi-value field widgets (checkboxes / multiselect /
|
|
1150
|
+
radio) stringify at theirs — multi-value fields store string arrays.
|
|
1151
|
+
|
|
1152
|
+
Round-trip pinned by real Radix interactions in jsdom: the in-form select
|
|
1153
|
+
submits `2` (number), the standalone select hands its handler `false`
|
|
1154
|
+
(boolean).
|
|
1155
|
+
|
|
1156
|
+
- aa35561: fix(form): a split create/edit form no longer loses the panel you are not submitting from (#2153)
|
|
1157
|
+
|
|
1158
|
+
`SplitForm` rendered one `SchemaRenderer` — one react-hook-form instance and one
|
|
1159
|
+
`<form>` element — **per section**, and its two groups of sections live in
|
|
1160
|
+
separate resizable panels. So each panel owned isolated form state: submitting
|
|
1161
|
+
from one panel's action bar sent only that section's fields and silently dropped
|
|
1162
|
+
everything the user had typed on the other side of the divider. Filling both
|
|
1163
|
+
panels and clicking Create persisted `{ subject }` alone.
|
|
1164
|
+
|
|
1165
|
+
The same isolation killed cross-panel field rules: a `visibleWhen` in the right
|
|
1166
|
+
panel referencing a left-panel field never saw that field in its record, so the
|
|
1167
|
+
predicate faulted and failed **open** — the field the author meant to hide was
|
|
1168
|
+
always shown.
|
|
1169
|
+
|
|
1170
|
+
Both panels are now ONE form. The panel group became a layout the form renderer
|
|
1171
|
+
owns, via a new `FormSchema.fieldPanes` (+ `fieldPanesOrientation`,
|
|
1172
|
+
`fieldPanesResizable`) that mirrors `fieldTabs` (#2959): the `<form>` wraps the
|
|
1173
|
+
whole `ResizablePanelGroup` and each pane holds only fields, which is what lets a
|
|
1174
|
+
single react-hook-form instance span the divider. Sections inside a pane render
|
|
1175
|
+
behind the inline `section-divider` header, each at its own declared column
|
|
1176
|
+
density within the form's shared grid.
|
|
1177
|
+
|
|
1178
|
+
One more fix falls out of moving the panels into the renderer: `splitResizable:
|
|
1179
|
+
false` now actually pins the divider. It previously only hid the grip — the
|
|
1180
|
+
separator stayed draggable, because nothing passed the panel library's
|
|
1181
|
+
`disabled`.
|
|
1182
|
+
|
|
1183
|
+
Each pane is its own `@container`, so a multi-column section collapses to fewer
|
|
1184
|
+
columns as its panel is dragged narrower instead of overflowing.
|
|
1185
|
+
|
|
1186
|
+
- 3c1f321: fix(form): a tabbed/sectioned create-edit form no longer loses the tabs you are not looking at (#2959, #2153)
|
|
1187
|
+
|
|
1188
|
+
The explicit-`sections` path rendered one `SchemaRenderer` — one react-hook-form
|
|
1189
|
+
instance and one `<form>` element — **per section**, all sharing the same
|
|
1190
|
+
`formId`. Two failures compounded:
|
|
1191
|
+
|
|
1192
|
+
1. the footer submit button (`form={formId}`) can only be associated with the
|
|
1193
|
+
**first** of those forms, so section 2+ never reached the payload; and
|
|
1194
|
+
2. in the `tabbed` variant Radix unmounted the inactive panel, destroying that
|
|
1195
|
+
tab's form state outright.
|
|
1196
|
+
|
|
1197
|
+
Reported flow (HotCRM, 3 tabs, required `description` on tab 3): fill tab 1 →
|
|
1198
|
+
submit → server 400 `description is required` → switch to tab 3, fill it →
|
|
1199
|
+
submit → the server now reports `subject; description; status; priority` **all**
|
|
1200
|
+
missing, because the second submit's body had lost every earlier value.
|
|
1201
|
+
|
|
1202
|
+
`ModalForm` (stacked and `contentLayout: 'tabbed'`) and `TabbedForm` now render
|
|
1203
|
+
ONE form for all sections, matching `ObjectForm` / `DrawerForm`. Stacked sections
|
|
1204
|
+
use the existing inline `section-divider` header (which now also renders the
|
|
1205
|
+
section's `description`); tabbed sections go through a new
|
|
1206
|
+
`FormSchema.fieldTabs` (+ `defaultFieldTab`, `fieldTabsPosition`) that the form
|
|
1207
|
+
renderer distributes into **force-mounted** Radix panels — CSS-hidden rather
|
|
1208
|
+
than unmounted, since react-hook-form skips validation for unmounted fields,
|
|
1209
|
+
which is how a required field on a tab nobody opened used to sail past the
|
|
1210
|
+
client and come back as a server 400.
|
|
1211
|
+
|
|
1212
|
+
Validation feedback now points at the tab: a rejected field activates its tab and
|
|
1213
|
+
every tab holding one is marked on its trigger, for client-side rules and server
|
|
1214
|
+
`fields[]` rejections alike.
|
|
1215
|
+
|
|
1216
|
+
- Updated dependencies [62311b6]
|
|
1217
|
+
- Updated dependencies [fc0272a]
|
|
1218
|
+
- Updated dependencies [9e7349e]
|
|
1219
|
+
- Updated dependencies [8864971]
|
|
1220
|
+
- Updated dependencies [1cf0de7]
|
|
1221
|
+
- Updated dependencies [752e18f]
|
|
1222
|
+
- Updated dependencies [c785740]
|
|
1223
|
+
- Updated dependencies [b41f401]
|
|
1224
|
+
- Updated dependencies [19e9fa0]
|
|
1225
|
+
- Updated dependencies [d61efd1]
|
|
1226
|
+
- Updated dependencies [95b7214]
|
|
1227
|
+
- Updated dependencies [7d9734d]
|
|
1228
|
+
- Updated dependencies [6ae818e]
|
|
1229
|
+
- Updated dependencies [9eb932b]
|
|
1230
|
+
- Updated dependencies [746dd00]
|
|
1231
|
+
- Updated dependencies [aebfa4f]
|
|
1232
|
+
- Updated dependencies [38ca8be]
|
|
1233
|
+
- Updated dependencies [3cb9646]
|
|
1234
|
+
- Updated dependencies [4952edf]
|
|
1235
|
+
- Updated dependencies [7f0252e]
|
|
1236
|
+
- Updated dependencies [c4d7b20]
|
|
1237
|
+
- Updated dependencies [7639a61]
|
|
1238
|
+
- Updated dependencies [94e63ef]
|
|
1239
|
+
- Updated dependencies [02aef0c]
|
|
1240
|
+
- Updated dependencies [6f29aa5]
|
|
1241
|
+
- Updated dependencies [d21794c]
|
|
1242
|
+
- Updated dependencies [c4db402]
|
|
1243
|
+
- Updated dependencies [5319bf1]
|
|
1244
|
+
- Updated dependencies [49e5671]
|
|
1245
|
+
- Updated dependencies [b5b97e2]
|
|
1246
|
+
- Updated dependencies [f59f2c1]
|
|
1247
|
+
- Updated dependencies [07de839]
|
|
1248
|
+
- Updated dependencies [2a40b5e]
|
|
1249
|
+
- Updated dependencies [4874117]
|
|
1250
|
+
- Updated dependencies [ad0183a]
|
|
1251
|
+
- Updated dependencies [ce08d55]
|
|
1252
|
+
- Updated dependencies [32462dd]
|
|
1253
|
+
- Updated dependencies [aa1240a]
|
|
1254
|
+
- Updated dependencies [2374a49]
|
|
1255
|
+
- Updated dependencies [390c071]
|
|
1256
|
+
- Updated dependencies [d10f526]
|
|
1257
|
+
- Updated dependencies [2d5d594]
|
|
1258
|
+
- Updated dependencies [ea7f477]
|
|
1259
|
+
- Updated dependencies [7f23cd0]
|
|
1260
|
+
- Updated dependencies [0ded602]
|
|
1261
|
+
- Updated dependencies [24e0e0a]
|
|
1262
|
+
- Updated dependencies [f8a95e5]
|
|
1263
|
+
- Updated dependencies [3a6cf24]
|
|
1264
|
+
- Updated dependencies [aa35561]
|
|
1265
|
+
- Updated dependencies [03bd53b]
|
|
1266
|
+
- Updated dependencies [3c1f321]
|
|
1267
|
+
- Updated dependencies [a045a32]
|
|
1268
|
+
- Updated dependencies [912496d]
|
|
1269
|
+
- Updated dependencies [9867281]
|
|
1270
|
+
- @object-ui/core@17.1.0
|
|
1271
|
+
- @object-ui/react@17.1.0
|
|
1272
|
+
- @object-ui/types@17.1.0
|
|
1273
|
+
- @object-ui/i18n@17.1.0
|
|
1274
|
+
- @object-ui/sdui-parser@17.1.0
|
|
1275
|
+
- @object-ui/react-runtime@17.1.0
|
|
1276
|
+
|
|
1277
|
+
## 17.0.0
|
|
1278
|
+
|
|
1279
|
+
### Patch Changes
|
|
1280
|
+
|
|
1281
|
+
- 7b21891: fix(action): honor the spec `disabled` predicate on every action-rendering surface (#1885 follow-through)
|
|
1282
|
+
|
|
1283
|
+
The spec Action field is `disabled` (boolean | CEL — disabled when TRUE); the
|
|
1284
|
+
schema has no `enabled` key. #1885 wired it in `action:button` only. Browser
|
|
1285
|
+
dogfooding against the showcase found FIVE more surfaces where a spec-authored
|
|
1286
|
+
`disabled` silently did nothing:
|
|
1287
|
+
|
|
1288
|
+
- **components** — the `action:group` leaves (inline + dropdown), `action:icon`
|
|
1289
|
+
and `action:menu` still read the legacy non-spec `enabled`. They now consume
|
|
1290
|
+
`disabled` as the primary control (evaluated in the same scope as `visible`),
|
|
1291
|
+
with `enabled` kept as a deprecated fallback.
|
|
1292
|
+
- **app-shell** — `DeclaredActionsBar` (server-declared action bar) read
|
|
1293
|
+
neither; it gains `disabled` (no legacy fallback: declared actions are
|
|
1294
|
+
spec-shaped and never carried `enabled`).
|
|
1295
|
+
- **plugin-detail** — `record:quick_actions` HAD a `disabled` implementation,
|
|
1296
|
+
but its `typeof === 'string'` split dropped the `{dialect:'cel', source}`
|
|
1297
|
+
envelope the server compiles authored CEL into (#2661 routes envelopes to the
|
|
1298
|
+
canonical formula engine), so the predicate never fired on real metadata. It
|
|
1299
|
+
now feeds `toPredicateInput`'s result to `useCondition` whole, like every
|
|
1300
|
+
other surface.
|
|
1301
|
+
|
|
1302
|
+
Pinned by new `DropdownActionItem` tests (disabled-when-TRUE, false-stays-
|
|
1303
|
+
clickable, disabled-wins-over-enabled, boolean literal) and browser-verified
|
|
1304
|
+
end-to-end against the showcase `showcase_archive_task` specimen: greyed on an
|
|
1305
|
+
in-progress task, clickable on a done one (with `visible` hiding Mark Done on
|
|
1306
|
+
the same screen — the hide-vs-grey contrast).
|
|
1307
|
+
|
|
1308
|
+
- de5e40c: fix(approvals): Approval Center UX pass — badge nowrap, approve confirm, decision progress bar, localized declared actions (#2762)
|
|
1309
|
+
|
|
1310
|
+
- **Badge no longer stacks CJK text vertically (P0-1)** — `Badge` gains
|
|
1311
|
+
`whitespace-nowrap` in its base variants (a badge is a single-line pill by
|
|
1312
|
+
definition), and the inbox 状态 column gets a minimum width, so 待审批 can
|
|
1313
|
+
never render as 待/审/批.
|
|
1314
|
+
- **Quick Approve now confirms (P0-2)** — the row's right-edge ✓, the mobile
|
|
1315
|
+
card button and the `a` keyboard shortcut all route through a confirmation
|
|
1316
|
+
dialog before executing, mirroring the Reject flow; an irreversible decision
|
|
1317
|
+
can no longer fire on a stray click.
|
|
1318
|
+
- **Decision progress is visualized (P1-1)** — the drawer renders a segmented
|
|
1319
|
+
progress bar (ARIA `progressbar`) for `decision_progress`, per-group chips
|
|
1320
|
+
get an explicit unsatisfied ○ state next to the satisfied ✓, the eligible
|
|
1321
|
+
approver count is spelled out, and the drawer pager now reads
|
|
1322
|
+
"Request N of M" so it can't be misread as approval progress.
|
|
1323
|
+
- **Declared action labels localize (P0-3)** — `DeclaredActionsBar` resolves
|
|
1324
|
+
label / confirmText / successMessage through the `_actions.<name>.*`
|
|
1325
|
+
translation convention (metadata literals as fallback), matching
|
|
1326
|
+
ObjectView/RecordDetailView; with the `@objectstack/plugin-approvals`
|
|
1327
|
+
bundle, the drawer shows 通过 / 拒绝 / 转签 instead of English in a zh-CN
|
|
1328
|
+
workspace. New `approvalsInbox` keys shipped in all ten locales.
|
|
1329
|
+
|
|
1330
|
+
- 8fb1295: fix(data-table): keep the right-pinned action column HEADER sticky on horizontal scroll (objectui#2784)
|
|
1331
|
+
|
|
1332
|
+
The row-actions column is pinned to the right edge by injecting `sticky right-0`
|
|
1333
|
+
into the column's `className`, which reaches both the body cells and the header
|
|
1334
|
+
cell. Body cells stayed pinned, but the header cell unconditionally appended a
|
|
1335
|
+
`relative` position utility (it anchors the column-resize handle) — and since
|
|
1336
|
+
`cn` is `tailwind-merge`, the later `relative` won over the injected `sticky`.
|
|
1337
|
+
So the "操作" title scrolled away while its body cells stayed frozen.
|
|
1338
|
+
|
|
1339
|
+
The header now detects a right-pinned column (its `className` carries
|
|
1340
|
+
`sticky` + `right-0`), skips `relative` for it (a sticky cell is already its own
|
|
1341
|
+
positioning context, so the `absolute` resize handle still anchors correctly),
|
|
1342
|
+
and re-asserts `sticky right-0 z-20` after `col.className` so tailwind-merge
|
|
1343
|
+
keeps the pin and it stacks above the body's pinned cells (z-10). Left-frozen
|
|
1344
|
+
columns, the resize handle, and non-pinned columns are unaffected.
|
|
1345
|
+
|
|
1346
|
+
- c19ac11: fix(form): scroll and focus the first errored field on an invalid submit (#2793)
|
|
1347
|
+
|
|
1348
|
+
Submitting a form with a missing required field already toasts the offending field names (#2329), but in a long form the field itself stays off-screen, so the user still hunts for it. react-hook-form's native focus-on-error only reaches fields whose registered ref is a focusable native input — it silently no-ops for custom widgets (lookup / select / master-detail), which is exactly the reported case. The form renderer now disables RHF's unreliable `shouldFocusError` and, in its `onInvalid` handler, scrolls the first errored field (in visual/declared order) into view via its `data-field` wrapper and focuses a focusable control inside it — working for every field type.
|
|
1349
|
+
|
|
1350
|
+
- 2f947e4: fix(page,field): consume the spec's `type`/`label`/`maxLength` keys (framework#1878 §3 naming-drift recheck)
|
|
1351
|
+
|
|
1352
|
+
Three forward-drifts where objectui read a different key than the spec
|
|
1353
|
+
declares, so authoring the documented key silently no-oped:
|
|
1354
|
+
|
|
1355
|
+
- **page `type` → `pageType`** (app-shell + components): `PageSchema` declares
|
|
1356
|
+
the page KIND as `type`, but `PageRenderer` reads `schema.pageType` and fell
|
|
1357
|
+
back to `'record'` — and nothing mapped between them. Every non-record page
|
|
1358
|
+
(`home`/`app`/`list`/`utility`) rendered with the record max-width, a wrong
|
|
1359
|
+
`data-page-type` attribute, and a suppressed header. `PageView` now passes
|
|
1360
|
+
`pageType` alongside the SchemaNode discriminator `type`.
|
|
1361
|
+
- **page `label` → `title`** (components): `PageSchema.label` is required but the
|
|
1362
|
+
region renderer read only `title`. Now dual-reads `title ?? label`, mirroring
|
|
1363
|
+
the fallback `DashboardRenderer` already uses. Coupled with the above — the
|
|
1364
|
+
header is gated on `pageType !== 'record'`, so both were needed for a title to
|
|
1365
|
+
appear.
|
|
1366
|
+
- **field `maxLength`/`minLength`** (plugin-form + fields): validation already
|
|
1367
|
+
dual-read these, but `ObjectForm`'s HTML-attribute pass and `TextAreaField`
|
|
1368
|
+
read `max_length` only, so a spec-authored `maxLength` gave no browser cap and
|
|
1369
|
+
no character counter. Both now dual-read, matching `buildValidationRules`.
|
|
1370
|
+
|
|
1371
|
+
Verified in the browser against the showcase: `capability_map` (`type: 'home'`)
|
|
1372
|
+
now renders `data-page-type="home"`, the `home` max-width and its page title;
|
|
1373
|
+
record pages are unchanged.
|
|
1374
|
+
|
|
1375
|
+
- 7d46648: fix(hooks): stop calling translation hooks inside try/catch (objectui#2879)
|
|
1376
|
+
|
|
1377
|
+
Eleven call sites wrapped a React hook in `try`/`catch` to make it
|
|
1378
|
+
"provider-safe". `useObjectTranslation` and `useObjectLabel` already are — they
|
|
1379
|
+
read context optionally and fall back to react-i18next's global instance, and
|
|
1380
|
+
never throw. The `catch` bought nothing and cost correctness: a throw _after_
|
|
1381
|
+
the hook ran desyncs hook order on the next render, because React matches hooks
|
|
1382
|
+
positionally. objectui#2595/#2596 fixed exactly this in `@object-ui/i18n`'s
|
|
1383
|
+
`createSafeTranslation`; nine plugin-local re-implementations kept their own
|
|
1384
|
+
copy of the bug, and two more (`ObjectTimeline`, `ObjectView`) were found by the
|
|
1385
|
+
new lint rule below — `ObjectView` had even suppressed
|
|
1386
|
+
`react-hooks/rules-of-hooks` inline to keep it.
|
|
1387
|
+
|
|
1388
|
+
- Six exact re-implementations now delegate to `createSafeTranslation`:
|
|
1389
|
+
`plugin-detail`, `plugin-timeline`, `plugin-list`, `plugin-calendar`,
|
|
1390
|
+
`plugin-grid`'s `ObjectGrid`, `plugin-designer`.
|
|
1391
|
+
- `components`' `data-table` also delegates; `createSafeTranslation` now
|
|
1392
|
+
returns `language` alongside `t` so consumers that localize dates don't need
|
|
1393
|
+
a second hook call. Purely additive.
|
|
1394
|
+
- `plugin-gantt` and `plugin-grid`'s `ImportWizard` keep their local hooks —
|
|
1395
|
+
they fall back _per key_, which a single-probe factory cannot express and
|
|
1396
|
+
which their comments justify (a host dictionary that covers common keys but
|
|
1397
|
+
lags on newer ones). Only the `try`/`catch` is removed.
|
|
1398
|
+
- `ObjectTimeline` and `ObjectView` call the hook directly and probe the
|
|
1399
|
+
returned value, mirroring `useSafeFieldLabel`.
|
|
1400
|
+
|
|
1401
|
+
Adds `object-ui/no-try-catch-around-hook` (error) so a twelfth copy fails CI.
|
|
1402
|
+
It only matches `use*` names, accepts member calls solely on `React` (so
|
|
1403
|
+
`vi.useRealTimers()` is not a hook), and resets its try-depth inside nested
|
|
1404
|
+
functions (so `renderHook(() => useThing())` inside a `try` is fine) — both
|
|
1405
|
+
false positives were real code in this repo and are pinned in the rule's tests.
|
|
1406
|
+
|
|
1407
|
+
`eslint-rules/**/*.test.js` matched no vitest project glob, so the local
|
|
1408
|
+
plugin's specs had never run in CI. They are now included; all three pass.
|
|
1409
|
+
|
|
1410
|
+
`ObjectTimeline`'s test mock of `@object-ui/react` omitted `useObjectLabel` —
|
|
1411
|
+
the removed `try`/`catch` had been silently absorbing that gap. The mock is now
|
|
1412
|
+
complete.
|
|
1413
|
+
|
|
1414
|
+
- 9b53d72: feat(charts): ObjectChart honors the spec `ChartConfig` author shape (objectui#2880 / framework#3729)
|
|
1415
|
+
|
|
1416
|
+
`ChartConfigSchema` is the chart protocol, but the renderer only ever read a
|
|
1417
|
+
Recharts-flavoured internal shape — `chartType`, `xAxisKey`, `series[].dataKey`.
|
|
1418
|
+
Everything an author wrote in the SPEC shape reached the renderer and was
|
|
1419
|
+
silently dropped, which is exactly what ADR-0078 forbids. framework#3725
|
|
1420
|
+
documented the gap by trimming the published contract down to the props that
|
|
1421
|
+
actually worked; this closes it the other way round.
|
|
1422
|
+
|
|
1423
|
+
**S1 — one normalization boundary.** `normalizeChartSchema` translates the
|
|
1424
|
+
author shape into the internal pipeline contract in a single place, rather than
|
|
1425
|
+
scattering `??` fallbacks through the render tree (framework PD #12: one
|
|
1426
|
+
translation is a contract mapping, N fallbacks are a second dialect):
|
|
1427
|
+
|
|
1428
|
+
- `type` → `chartType`, `xAxis: { field }` → `xAxisKey`, `series: [{ name }]` →
|
|
1429
|
+
`series: [{ dataKey }]`
|
|
1430
|
+
- the report surface's bare-string `xAxis`/`yAxis` resolve too
|
|
1431
|
+
- `yAxis: [{ field }]` alone plots, with no `series` declared
|
|
1432
|
+
- **internal props win**, so `DashboardRenderer`, `ObjectView` and the dataset
|
|
1433
|
+
path are byte-for-byte unaffected — there is no migration
|
|
1434
|
+
|
|
1435
|
+
**The `type` collision.** `ChartConfig.type` is the chart family, but on any
|
|
1436
|
+
surface that flattens chart config into a props bag `type` is already the SDUI
|
|
1437
|
+
envelope's component discriminator. Spreading props last let an author's
|
|
1438
|
+
`type="bar"` replace `object-chart` so the block stopped resolving; stamping the
|
|
1439
|
+
discriminator last ate the author's value instead. The react-page wrapper now
|
|
1440
|
+
keeps both: the discriminator wins the `type` slot and the author's value is
|
|
1441
|
+
preserved beside it as `specType`, which the normalizer reads back.
|
|
1442
|
+
|
|
1443
|
+
**S2 — axis presentation.** `ChartAxis.format` drives the tick formatter (via
|
|
1444
|
+
`Intl.NumberFormat`, no new dependency), `min`/`max` pin the domain,
|
|
1445
|
+
`logarithmic` swaps the scale, `title` labels the axis, and `showGridLines` is
|
|
1446
|
+
honored. A second `yAxis` entry (or `position: 'right'`) turns on the secondary
|
|
1447
|
+
axis that `series[].yAxis` binds to — in combo charts an explicit binding now
|
|
1448
|
+
beats the family-derived bar→left/line→right guess. `showLegend` is honored,
|
|
1449
|
+
and `title`/`subtitle` render above the plot instead of only titling the
|
|
1450
|
+
drill-down drawer.
|
|
1451
|
+
|
|
1452
|
+
**S3 — `series[].stack`, `annotations`, `interaction`.** Stacking passes the
|
|
1453
|
+
author's group name through as Recharts' `stackId`. Annotations render as
|
|
1454
|
+
`ReferenceLine` (`type: 'line'`) / `ReferenceArea` (`type: 'region'`) with the
|
|
1455
|
+
declared axis, colour, style and label. `interaction.tooltips: false` suppresses
|
|
1456
|
+
the hover card and `interaction.brush: true` adds the range selector;
|
|
1457
|
+
`showDataLabels` prints values on the marks. `interaction.zoom` has no Recharts
|
|
1458
|
+
primitive behind it and is deliberately still unimplemented rather than faked.
|
|
1459
|
+
|
|
1460
|
+
- 53642d4: fix(core,fields): a string `$orderby` is a clause, not a character array — and localize the sharing-rule widgets (objectstack#3821)
|
|
1461
|
+
|
|
1462
|
+
**The recipient picker listed nothing, ever.** `QueryParams['$orderby']` was
|
|
1463
|
+
typed as `Record | string[] | SortObject[]`, so `queryParamsToRecord` sent any
|
|
1464
|
+
non-array value through `Object.entries`. Handed the clause string `'name asc'`
|
|
1465
|
+
— which callers do build by hand — it walked the string index by index and
|
|
1466
|
+
emitted `$orderby=0 n,1 a,2 m,3 e,4 ,5 a,6 s,7 c`. The server sorted by columns
|
|
1467
|
+
that don't exist and every row was filtered out, so
|
|
1468
|
+
`sys_sharing_rule.recipient_id` rendered "No matches" for every recipient type
|
|
1469
|
+
and no sharing rule could be created from the Console. `ObjectGrid` builds the
|
|
1470
|
+
same shape from a schema-level `sort` in three places, so grids with a string
|
|
1471
|
+
sort silently showed an empty table.
|
|
1472
|
+
|
|
1473
|
+
A string `$orderby` is now passed through verbatim (the server's OData
|
|
1474
|
+
normalizer has always parsed `'name asc'`), and the type admits `string`.
|
|
1475
|
+
`RecipientPickerField` additionally switched to the structured
|
|
1476
|
+
`{ name: 'asc' }` form so it can't regress this way against any data source.
|
|
1477
|
+
|
|
1478
|
+
**The three sharing-rule authoring widgets never had translations.**
|
|
1479
|
+
`ObjectRefField`, `RecipientPickerField` and `FilterConditionField` hardcoded
|
|
1480
|
+
their English copy — a Chinese Console showed "Select an object", "Select a
|
|
1481
|
+
user", "Search…", "No matches", "Edit as JSON". They now go through
|
|
1482
|
+
`useFieldTranslation` like every other widget, with keys added under `fields.*`
|
|
1483
|
+
in all ten locales.
|
|
1484
|
+
|
|
1485
|
+
The recipient placeholder was the interesting one: it read
|
|
1486
|
+
`` `Select a ${recipientType.replace(/_/g,' ')}` ``, interpolating the enum
|
|
1487
|
+
value into an English sentence — a shape no locale can translate. It is now a
|
|
1488
|
+
per-type key (`fields.recipient.selectUser`, `…selectBusinessUnit`, …), so
|
|
1489
|
+
"选择业务单元" and "Select a business unit" no longer have to share a structure.
|
|
1490
|
+
|
|
1491
|
+
**Editing a rule silently dropped its recipient.** The picker resets the stored
|
|
1492
|
+
id when `recipient_type` changes, because an id valid for a user is meaningless
|
|
1493
|
+
for a team. It treated the edit form's `'' → 'user'` hydration as such a change:
|
|
1494
|
+
opening any saved rule blanked the recipient, and saving persisted the blank.
|
|
1495
|
+
Only a non-empty predecessor now counts as a type switch.
|
|
1496
|
+
|
|
1497
|
+
**Building a filter submitted the surrounding form.** None of `FilterBuilder`'s
|
|
1498
|
+
controls declared `type="button"`, and a bare `<button>` inside a `<form>`
|
|
1499
|
+
defaults to `type="submit"`. Adding, removing or clearing a condition therefore
|
|
1500
|
+
submitted the sharing-rule dialog — firing validation mid-edit, and on an
|
|
1501
|
+
already-valid form saving the record before the admin was done.
|
|
1502
|
+
|
|
1503
|
+
**A rejected write showed the user raw server diagnostics.** The form rendered
|
|
1504
|
+
`error.message` verbatim, so a sharing / RLS denial reached the dialog and the
|
|
1505
|
+
toast as `FORBIDDEN: insufficient privileges to update showcase_private_note
|
|
1506
|
+
pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and the
|
|
1507
|
+
record id to whoever hit it. Permission failures now render localized copy
|
|
1508
|
+
(`form.noPermissionToSave`, added in all ten locales), with the server text kept
|
|
1509
|
+
on the console for debugging; other failures still show the server's message,
|
|
1510
|
+
which is the useful part, and fall back to `form.submitFailed` when there is
|
|
1511
|
+
none — replacing the previously hardcoded English "An error occurred during
|
|
1512
|
+
submission".
|
|
1513
|
+
|
|
1514
|
+
**The detail header offered "Edit" on records the user may only read.** Object
|
|
1515
|
+
permissions can't express "this one record is read-only" — a read-only sharing
|
|
1516
|
+
grant sits inside an object the user may otherwise edit — so the header showed
|
|
1517
|
+
the primary Edit CTA, opened the form, and let the user retype a field before
|
|
1518
|
+
the server rejected the save. `DetailView` now gates Edit / Delete on the
|
|
1519
|
+
object-level check AND on the explain engine's record-grained verdict
|
|
1520
|
+
(`POST /api/v1/security/explain` with a `recordId`, ADR-0090 D6 / ADR-0095 C2 —
|
|
1521
|
+
the same pipeline the enforcement middleware runs, so button and server cannot
|
|
1522
|
+
disagree). Explaining oneself needs no special permission. The probe is one
|
|
1523
|
+
cached request per record, skipped entirely when the object-level check already
|
|
1524
|
+
says no, and **fails open** on every uncertainty — an unanswered hint must never
|
|
1525
|
+
be the reason a permitted user cannot act; the server stays the authority
|
|
1526
|
+
(ADR-0057 D10).
|
|
1527
|
+
|
|
1528
|
+
**A long option rendered straight past the combobox border.** `Combobox`'s
|
|
1529
|
+
trigger pinned itself to the component's `w-[200px]` default while the fields
|
|
1530
|
+
around it ran the full form column, and the selected label was a bare text child
|
|
1531
|
+
of a flex button — flex items need `truncate` AND `min-w-0` to clip, and it had
|
|
1532
|
+
neither. So "成员 (showcase_project_membership)" in the object picker overflowed
|
|
1533
|
+
the control and collided with the field beside it. The label now truncates, the
|
|
1534
|
+
trigger can shrink, the dropdown matches the trigger's width instead of a
|
|
1535
|
+
hardcoded 200px (a widened combobox used to clip its own options), and the two
|
|
1536
|
+
sharing-rule pickers ask for `w-full` so they line up with every other input.
|
|
1537
|
+
|
|
1538
|
+
Hardens `evaluatePermission` while there: a role config carrying only
|
|
1539
|
+
`fieldPermissions` (no `actions`) made `check()` throw a TypeError that
|
|
1540
|
+
propagated out of the render. A permission check must not be able to crash a
|
|
1541
|
+
view.
|
|
1542
|
+
|
|
1543
|
+
Browser-verified against the framework showcase Console in Chinese: object /
|
|
1544
|
+
criteria / recipient copy is fully localized, the recipient dropdown lists real
|
|
1545
|
+
users, business units and positions, a saved rule reopens with its recipient and
|
|
1546
|
+
criteria intact, editing the filter no longer submits, and a rule created
|
|
1547
|
+
end-to-end stores a real record id rather than free text. The criteria authored
|
|
1548
|
+
in the builder is honored by the evaluator: `{"pinned":true}` on an owner-private
|
|
1549
|
+
object granted the recipient exactly the matching records and nothing else.
|
|
1550
|
+
|
|
1551
|
+
- c6aaed8: fix(i18n): retire four hand-rolled zh/en branches (objectui#2871, part 1)
|
|
1552
|
+
|
|
1553
|
+
Four surfaces decided their language with a hand-written `startsWith('zh')`
|
|
1554
|
+
check instead of the locale packs, so the other eight shipped languages
|
|
1555
|
+
silently rendered English and the strings could never be translated without a
|
|
1556
|
+
code change.
|
|
1557
|
+
|
|
1558
|
+
- **`RecordTitleChip`** carried a private zh-CN/zh-TW dictionary behind a
|
|
1559
|
+
comment claiming "components is i18n-free". That is not true —
|
|
1560
|
+
`@object-ui/components` declares `@object-ui/i18n` and its sibling
|
|
1561
|
+
`containers.tsx` already uses it. All four of its keys (`detail.copied`,
|
|
1562
|
+
`detail.copyRecordId`, `detail.addToFavorites`, `detail.removeFromFavorites`)
|
|
1563
|
+
already existed in **all ten packs**, so this deletes ~35 lines and fixes ten
|
|
1564
|
+
locales with zero new translations. It renders on every record detail page.
|
|
1565
|
+
- **`EnvironmentListToolbar`**'s three state-aware CTA labels move to a new
|
|
1566
|
+
`environment.*` namespace. This surface had already regressed once for the
|
|
1567
|
+
same reason (#844) and was fixed then with inline `{en,zh}` pairs.
|
|
1568
|
+
- **`StudioAiCopilot`**'s dock title moves to the Studio catalog as
|
|
1569
|
+
`engine.studio.aiCopilot`.
|
|
1570
|
+
- **`StudioHomePage.relativeTime`** now uses `Intl.RelativeTimeFormat` with
|
|
1571
|
+
`numeric: 'auto'` instead of five `zh ? … : …` ternaries. This is strictly
|
|
1572
|
+
better than adding ten catalog keys: it covers every locale, applies the
|
|
1573
|
+
correct plural rules, and yields "yesterday" / 「昨天」 rather than "1d ago".
|
|
1574
|
+
Arabic gets its dual form («أسبوعين») — something a ternary cannot express.
|
|
1575
|
+
|
|
1576
|
+
The new `environment.*` keys are added to all ten packs, so this does not widen
|
|
1577
|
+
the gap tracked by objectui#2872 part (a).
|
|
1578
|
+
|
|
1579
|
+
`EnvironmentListToolbar`'s tests now render inside a real `I18nProvider` pinned
|
|
1580
|
+
to `en`. Without one, `t()` returns the raw key, so the previous assertions on
|
|
1581
|
+
literal English would have been asserting nothing.
|
|
1582
|
+
|
|
1583
|
+
- dc334da: fix(i18n): close the last three zh-branch gaps (objectui#2871, part 3)
|
|
1584
|
+
|
|
1585
|
+
The three items the #2871 classification marked as real but _not_ a
|
|
1586
|
+
migrate-the-copy fix. Each needed a different remedy.
|
|
1587
|
+
|
|
1588
|
+
**`LoadingScreen` — ten languages collapsed to two.** The boot splash already
|
|
1589
|
+
selected real locale packs (not inline copy), but through
|
|
1590
|
+
`lang.startsWith('zh') ? zh : en`, so a ja/ko/de user watched the whole startup
|
|
1591
|
+
in English. It now indexes `builtInLocales` by the two-letter prefix.
|
|
1592
|
+
|
|
1593
|
+
Each field falls back to `en` **individually**, which matters: `console.*` is
|
|
1594
|
+
one of the namespaces that trails in the non-`zh` packs (objectui#2872 part a),
|
|
1595
|
+
so a whole-object swap would have rendered `undefined` on the splash rather
|
|
1596
|
+
than English. `console.loadingHint` was in fact missing from all eight — added
|
|
1597
|
+
here, since a blank line under the progress list is worse than an English one.
|
|
1598
|
+
|
|
1599
|
+
**`containers.tsx` — two language sources that could disagree.** The tab-label
|
|
1600
|
+
call sites resolved `language` from `useObjectTranslation()`, then handed the
|
|
1601
|
+
string to `translateLabel`, which called `detectLocale()` and read
|
|
1602
|
+
`document.documentElement.lang` on its own. Those update independently, so an
|
|
1603
|
+
in-app language switch could leave a tab label and its surrounding chrome in
|
|
1604
|
+
different languages until the next reload. `language` is now threaded in, and
|
|
1605
|
+
`detectLocale` is deleted so nothing reaches for the DOM again.
|
|
1606
|
+
|
|
1607
|
+
**`field-types.ts` — a two-language data catalog.** `FieldTypeMeta` carried a
|
|
1608
|
+
`labelZh` column beside `label`, which capped the field-type picker at English
|
|
1609
|
+
or Chinese by construction. The 46 type names and 9 category names move into
|
|
1610
|
+
the Studio catalog as `engine.fieldType.<id>` / `engine.fieldCategory.<cat>`,
|
|
1611
|
+
generated from the existing values so no wording changes. This removes the
|
|
1612
|
+
`isZh` helper from **both** `ObjectFieldInspector` and `ObjectFormCanvas` — the
|
|
1613
|
+
two files the classification listed as "keep the component, fix the catalog".
|
|
1614
|
+
|
|
1615
|
+
The picker's search filter previously matched `id`, the English label, and
|
|
1616
|
+
`labelZh` — so searching in Japanese or German matched nothing. It now matches
|
|
1617
|
+
the label as the user actually sees it.
|
|
1618
|
+
|
|
1619
|
+
- Updated dependencies [0b3be01]
|
|
1620
|
+
- Updated dependencies [3c4d935]
|
|
1621
|
+
- Updated dependencies [4b60d2d]
|
|
1622
|
+
- Updated dependencies [952b978]
|
|
1623
|
+
- Updated dependencies [de5e40c]
|
|
1624
|
+
- Updated dependencies [1a03af6]
|
|
1625
|
+
- Updated dependencies [3e886eb]
|
|
1626
|
+
- Updated dependencies [cfc675e]
|
|
1627
|
+
- Updated dependencies [20df08c]
|
|
1628
|
+
- Updated dependencies [1767124]
|
|
1629
|
+
- Updated dependencies [8ecf5a6]
|
|
1630
|
+
- Updated dependencies [af705b9]
|
|
1631
|
+
- Updated dependencies [0502a7c]
|
|
1632
|
+
- Updated dependencies [7b35e4b]
|
|
1633
|
+
- Updated dependencies [e16ed2d]
|
|
1634
|
+
- Updated dependencies [c6fd752]
|
|
1635
|
+
- Updated dependencies [f9bbddb]
|
|
1636
|
+
- Updated dependencies [dfd3705]
|
|
1637
|
+
- Updated dependencies [c77108c]
|
|
1638
|
+
- Updated dependencies [2735de6]
|
|
1639
|
+
- Updated dependencies [6dee2cb]
|
|
1640
|
+
- Updated dependencies [e05f052]
|
|
1641
|
+
- Updated dependencies [0502a7c]
|
|
1642
|
+
- Updated dependencies [faad45e]
|
|
1643
|
+
- Updated dependencies [09c6a17]
|
|
1644
|
+
- Updated dependencies [c7cff19]
|
|
1645
|
+
- Updated dependencies [ba73a02]
|
|
1646
|
+
- Updated dependencies [cd09a7b]
|
|
1647
|
+
- Updated dependencies [f1abf0e]
|
|
1648
|
+
- Updated dependencies [f05b84e]
|
|
1649
|
+
- Updated dependencies [9b4b952]
|
|
1650
|
+
- Updated dependencies [7d46648]
|
|
1651
|
+
- Updated dependencies [bb4aa25]
|
|
1652
|
+
- Updated dependencies [75f1cdf]
|
|
1653
|
+
- Updated dependencies [662bdf9]
|
|
1654
|
+
- Updated dependencies [059a052]
|
|
1655
|
+
- Updated dependencies [53642d4]
|
|
1656
|
+
- Updated dependencies [8aae006]
|
|
1657
|
+
- Updated dependencies [c6cfdf1]
|
|
1658
|
+
- Updated dependencies [d147a13]
|
|
1659
|
+
- Updated dependencies [c6aaed8]
|
|
1660
|
+
- Updated dependencies [263f885]
|
|
1661
|
+
- Updated dependencies [dc334da]
|
|
1662
|
+
- @object-ui/i18n@17.0.0
|
|
1663
|
+
- @object-ui/react@17.0.0
|
|
1664
|
+
- @object-ui/types@17.0.0
|
|
1665
|
+
- @object-ui/core@17.0.0
|
|
1666
|
+
- @object-ui/react-runtime@17.0.0
|
|
1667
|
+
- @object-ui/sdui-parser@17.0.0
|
|
1668
|
+
|
|
3
1669
|
## 16.1.0
|
|
4
1670
|
|
|
5
1671
|
### Patch Changes
|