@object-ui/core 16.0.0 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1589 -1
- package/dist/actions/ActionRunner.d.ts +88 -16
- package/dist/actions/ActionRunner.js +164 -81
- package/dist/actions/actionKeys.d.ts +118 -0
- package/dist/actions/actionKeys.js +243 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/adapters/ApiDataSource.d.ts +11 -1
- package/dist/adapters/ApiDataSource.js +21 -1
- package/dist/adapters/ValueDataSource.d.ts +10 -2
- package/dist/adapters/ValueDataSource.js +9 -0
- package/dist/adapters/batchTransaction.d.ts +49 -0
- package/dist/adapters/batchTransaction.js +118 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +10 -0
- package/dist/evaluator/ExpressionEvaluator.js +48 -0
- package/dist/evaluator/SafeExpressionParser.js +5 -2
- package/dist/evaluator/fieldRules.d.ts +0 -2
- package/dist/evaluator/fieldRules.js +3 -2
- package/dist/evaluator/optionLint.js +5 -2
- package/dist/evaluator/optionRules.d.ts +35 -6
- package/dist/evaluator/optionRules.js +18 -23
- package/dist/index.d.ts +5 -0
- package/dist/index.js +8 -0
- package/dist/registry/Registry.d.ts +81 -2
- package/dist/registry/Registry.js +102 -2
- package/dist/registry/public-blocks.js +52 -1
- package/dist/theme/ThemeEngine.js +0 -12
- package/dist/utils/chart-series.d.ts +28 -0
- package/dist/utils/chart-series.js +55 -0
- package/dist/utils/column-identity.d.ts +110 -0
- package/dist/utils/column-identity.js +210 -0
- package/dist/utils/dashboard-filters.d.ts +2 -2
- package/dist/utils/dataset-format.d.ts +18 -1
- package/dist/utils/dataset-format.js +14 -1
- package/dist/utils/expand-fields.js +8 -8
- package/dist/utils/export-filename.js +4 -1
- package/dist/utils/filter-converter.d.ts +47 -0
- package/dist/utils/filter-converter.js +94 -7
- package/dist/utils/filter-tokens.d.ts +101 -0
- package/dist/utils/filter-tokens.js +173 -0
- package/dist/utils/managedBy.d.ts +119 -0
- package/dist/utils/managedBy.js +147 -0
- package/dist/utils/normalize-list-view.d.ts +99 -0
- package/dist/utils/normalize-list-view.js +255 -0
- package/dist/utils/record-title.js +0 -0
- package/dist/utils/sort-values.d.ts +52 -0
- package/dist/utils/sort-values.js +125 -0
- package/dist/validation/index.d.ts +9 -1
- package/dist/validation/index.js +9 -1
- package/dist/validation/validators/index.d.ts +7 -1
- package/dist/validation/validators/index.js +7 -1
- package/dist/validation/validators/object-validation-engine.d.ts +151 -31
- package/dist/validation/validators/object-validation-engine.js +275 -142
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1593 @@
|
|
|
1
1
|
# @object-ui/core
|
|
2
2
|
|
|
3
|
+
## 17.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 62311b6: feat(core): inventory `ActionDef`'s keys and warn on the ones nothing reads — objectstack#4075 step 1
|
|
8
|
+
|
|
9
|
+
`ActionDef` ends with `[key: string]: any`, so it accepts any key of any type.
|
|
10
|
+
Deleting `ActionDef.execute` produced **zero** compile errors even though the
|
|
11
|
+
field had just been removed (objectui#2990), and stale metadata still authoring
|
|
12
|
+
`execute: 'markDone'` type-checks today. The same deletion against
|
|
13
|
+
`@object-ui/types`' `ActionSchema` — which has no index signature — correctly
|
|
14
|
+
produced `TS2353` at the authoring site. One of the two readers can catch a
|
|
15
|
+
retired key; the other is structurally incapable, which is how a typo (`targt`)
|
|
16
|
+
and a tombstoned key both reach a runner that then silently does nothing.
|
|
17
|
+
|
|
18
|
+
This is the non-breaking first step of the staged narrowing: it makes the key set
|
|
19
|
+
**visible** and warns on anything outside it, without changing a single type.
|
|
20
|
+
|
|
21
|
+
New exports from `@object-ui/core`:
|
|
22
|
+
|
|
23
|
+
- `ACTION_DEF_KEYS`, `SPEC_ACTION_KEYS`, `NAVIGATION_ALIAS_KEYS`,
|
|
24
|
+
`RETIRED_ACTION_KEYS`, `KNOWN_ACTION_KEYS` — the inventory.
|
|
25
|
+
- `classifyActionKeys(action)` — splits an action's own keys into `unknown` and
|
|
26
|
+
`retired`.
|
|
27
|
+
- `warnOnUnknownActionKeys(action)` — dev-mode only, warn-once. Called by
|
|
28
|
+
`ActionRunner.execute`, so no consumer wiring is needed.
|
|
29
|
+
|
|
30
|
+
A retired key gets a louder, more specific warning than an unknown one: an
|
|
31
|
+
unknown key is probably a typo, a retired key is metadata that used to work.
|
|
32
|
+
`execute` is not simply gone from the spec — it is a live **tombstone**, still
|
|
33
|
+
present in `ActionSchema` so the parser can reject it by name with the rename
|
|
34
|
+
prescription.
|
|
35
|
+
|
|
36
|
+
Nothing is rejected and no types changed, so existing metadata behaves exactly as
|
|
37
|
+
before. Promoting the legitimate keys to explicit optional fields, then removing
|
|
38
|
+
the index signature so `tsc` catches both typos and retired keys, are steps 2 and
|
|
39
|
+
3 of objectstack#4075.
|
|
40
|
+
|
|
41
|
+
- 9e7349e: **`target` is the only action handler slot — the `execute` alias is gone from the renderer (framework#3856).**
|
|
42
|
+
|
|
43
|
+
`ActionRunner.executeScript` read `action.target || action.execute`. That fallback
|
|
44
|
+
is unreachable against `@objectstack/spec` 17: `execute` is now a tombstoned key
|
|
45
|
+
(framework#3855) that the parser **rejects** with the rename prescription, so no
|
|
46
|
+
parsed action can carry it and the `||` could only ever yield `target`. Verified
|
|
47
|
+
against 17.0.0-rc.0 — an action declaring `execute` fails `ActionSchema.safeParse`,
|
|
48
|
+
and a `target` action's parsed output has no `execute` key at all.
|
|
49
|
+
|
|
50
|
+
Deleted rather than left as harmless residue: two handler slots is what let one
|
|
51
|
+
action run one script server-side and a different one client-side (framework#3713,
|
|
52
|
+
where this renderer preferred the alias while the spec transform preferred
|
|
53
|
+
`target`). A dead slot still reads as a live contract to the next maintainer.
|
|
54
|
+
|
|
55
|
+
`execute` is also **removed from the types**, which is the part that had never
|
|
56
|
+
landed. framework#3856 predicted a compile error here; there wasn't one, because
|
|
57
|
+
neither reader was typed against the spec's `z.infer`:
|
|
58
|
+
|
|
59
|
+
- `@object-ui/types` `ActionSchema` hand-declared `execute?: string`. Removed, so
|
|
60
|
+
`execute: '…'` now fails `tsc` at the authoring site (TS2353).
|
|
61
|
+
- `@object-ui/core` `ActionDef` hand-declared it too. Removed — but `ActionDef`
|
|
62
|
+
carries a `[key: string]: any` index signature, so stale hand-authored metadata
|
|
63
|
+
that never passed through the parser still compiles. For that path
|
|
64
|
+
`executeScript` now returns the rename prescription instead of a bare
|
|
65
|
+
"No script provided", matching the spec tombstone's rule that removing an
|
|
66
|
+
authorable key must be audible: silently binding no handler is the
|
|
67
|
+
"Mark Done does nothing" shape (framework#2169).
|
|
68
|
+
|
|
69
|
+
The four action renderers (`action:button`, `action:icon`, `action:menu`,
|
|
70
|
+
`action:group`) no longer forward `execute` into the runner, and Studio's
|
|
71
|
+
`ActionPreview` no longer falls back to it — previewing an alias-only draft as
|
|
72
|
+
"bound" contradicted the parse that rejects it on save.
|
|
73
|
+
|
|
74
|
+
Requires `@objectstack/spec` 17. Metadata still on the alias is rewritten by
|
|
75
|
+
`os migrate meta --from 16`.
|
|
76
|
+
|
|
77
|
+
- 6ae818e: feat(core): one column identity per column — `field` stamped at ingestion (#3104)
|
|
78
|
+
|
|
79
|
+
A column's field identity was resolved twice, with two different precedences
|
|
80
|
+
over the same `schema.columns` array, and the two halves disagreed:
|
|
81
|
+
|
|
82
|
+
- **request path** — `ListView`'s `$expand` and `$select` builders, and
|
|
83
|
+
`ObjectGrid.getSelectFields`, read `f?.field` and only `f?.field`.
|
|
84
|
+
- **render path** — the FLS gate, the hidden-field filter, `fieldOrder`, both
|
|
85
|
+
export branches and the hide-fields popover read
|
|
86
|
+
`f.name || f.fieldName || f.field` — name FIRST.
|
|
87
|
+
|
|
88
|
+
So `{ field: 'account', name: 'account_name' }` fetched `account` while the
|
|
89
|
+
renderer keyed off `account_name`, and `{ name: 'account' }` rendered a column
|
|
90
|
+
the request dropped entirely — neither `$select` nor `$expand` carried it. That
|
|
91
|
+
is the mechanism behind the "relation column shows a bare id / column is empty
|
|
92
|
+
/ sort does nothing / export is missing a column" defect class.
|
|
93
|
+
|
|
94
|
+
Per AGENTS.md #0.1 the fix is not another `?? name` at the read sites. Legacy
|
|
95
|
+
acceptance moves to the one boundary that already folds this view's vocabulary,
|
|
96
|
+
`normalizeListViewSchema`, which now also canonicalizes each column's identity.
|
|
97
|
+
|
|
98
|
+
New in `@object-ui/core`:
|
|
99
|
+
|
|
100
|
+
- `columnIdentity(entry)` — the single reader. Resolves `field` → `name` →
|
|
101
|
+
`fieldName`, canonical-first, so it agrees with `buildExpandFields` instead
|
|
102
|
+
of racing it. Handles bare-string columns.
|
|
103
|
+
- `normalizeColumnIdentity(entry)` / `normalizeColumnIdentities(columns)` — the
|
|
104
|
+
fold. Stamps `field`; a legacy key that is **already present** is mirrored
|
|
105
|
+
onto the same identity so name-first readers resolve what the request asked
|
|
106
|
+
for; a legacy key that is **absent is never invented**, and an
|
|
107
|
+
already-canonical column is returned by reference.
|
|
108
|
+
- `hasConflictingColumnIdentity(entry)` — true when a column's keys disagree.
|
|
109
|
+
- `CANONICAL_COLUMN_IDENTITY_KEY`, `LEGACY_COLUMN_IDENTITY_KEYS`,
|
|
110
|
+
`TABLE_ADAPTER_COLUMN_KEY`.
|
|
111
|
+
|
|
112
|
+
The fold **mirrors** rather than deleting the legacy key, unlike the other
|
|
113
|
+
folds in `normalizeListViewSchema`. Deleting would work inside this repo (every
|
|
114
|
+
name-first read falls through to `field`), but `columns` entries cross the
|
|
115
|
+
package boundary into host renderers and dropping `name` from under them is a
|
|
116
|
+
breaking change with no inventory. Deletion is a later call, once the in-repo
|
|
117
|
+
consumers read `columnIdentity()`.
|
|
118
|
+
|
|
119
|
+
Behaviour is unchanged for any column carrying a single identity key — every
|
|
120
|
+
read site resolves the same string it did before. The entries whose resolution
|
|
121
|
+
moves are exactly the ones where two sites already disagreed.
|
|
122
|
+
|
|
123
|
+
`accessorKey` is deliberately untouched: it is TanStack Table's own column key
|
|
124
|
+
(`TableColumn.accessorKey`), not ObjectStack metadata identity, and folding
|
|
125
|
+
across that boundary would fossilize the merge.
|
|
126
|
+
|
|
127
|
+
- 746dd00: feat(sdui): curate the page:\*, element:\* and action:\* families into the public contract
|
|
128
|
+
|
|
129
|
+
The AI-authoring vocabulary and the Studio page designer disagreed by thirteen
|
|
130
|
+
blocks: `PUBLIC_BLOCKS` carried one `page:` tag and one `element:` tag while
|
|
131
|
+
the designer palette — and @objectstack/spec's page schema — offered the whole
|
|
132
|
+
families. A block a human can drag in Studio was invisible to a model writing
|
|
133
|
+
the same page, which is the objectui#3006 state at 10× the scale.
|
|
134
|
+
|
|
135
|
+
Fifteen tags join the contract (36 → 42 → **57**), every one shipping a
|
|
136
|
+
renderer with declared inputs (objectui#3065):
|
|
137
|
+
|
|
138
|
+
- `page:` — tabs, card, accordion, section, footer, sidebar
|
|
139
|
+
- `element:` — text, number, button, definition-list, repeater
|
|
140
|
+
- `action:` — button, group, menu, icon
|
|
141
|
+
|
|
142
|
+
Five stay out, each with its reason recorded and guarded: `action:bar`
|
|
143
|
+
(`record:quick_actions` covers the record action strip; the spec blesses the
|
|
144
|
+
other four), `element:image` (duplicates the curated `image` — one spelling
|
|
145
|
+
per concept), and `element:record_picker` / `element:text_input` /
|
|
146
|
+
`element:metadata_viewer` (mirroring the Studio palette's own exclusions, so
|
|
147
|
+
the two vocabularies stay out for the same reasons rather than by
|
|
148
|
+
coincidence).
|
|
149
|
+
|
|
150
|
+
The console's reverse-coverage guard now sweeps all four semantic namespaces
|
|
151
|
+
instead of `record:` alone — checking only the namespace you just fixed is
|
|
152
|
+
exactly how the last 22 doubled keys went unnoticed (objectui#3037). A new
|
|
153
|
+
prop-less allowlist (`element:divider`, `page:section`, `page:footer`,
|
|
154
|
+
`page:sidebar`) keeps "declares no inputs" a pinned decision in both
|
|
155
|
+
directions: those four must stay at zero, everything else curated must declare
|
|
156
|
+
a surface.
|
|
157
|
+
|
|
158
|
+
- 38ca8be: refactor(fields): `requiredWhen` is the only required-predicate slot — drop the retired `conditionalRequired` alias
|
|
159
|
+
|
|
160
|
+
`@objectstack/spec` 17 (objectstack#3855) **retired** `Field.conditionalRequired`,
|
|
161
|
+
the long-deprecated alias of `requiredWhen`. ObjectUI carried a back-compat read
|
|
162
|
+
for it in seven places; all of them are removed.
|
|
163
|
+
|
|
164
|
+
The removal is safe because the spec did not merely _stop emitting_ the key — it
|
|
165
|
+
made authoring it **fail loudly**. `retiredKey()` declares the key as
|
|
166
|
+
`z.never()`, so:
|
|
167
|
+
|
|
168
|
+
- `z.input` types it as `never` — writing it is a `tsc` error at the authoring site;
|
|
169
|
+
- the parse **rejects** it (verified against `17.0.0-rc.0`), at both `FieldSchema`
|
|
170
|
+
and `ObjectSchema`, with the prescription as the message:
|
|
171
|
+
|
|
172
|
+
> `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use
|
|
173
|
+
> `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged.
|
|
174
|
+
> Run `os migrate meta --from 16` to rewrite it automatically.
|
|
175
|
+
|
|
176
|
+
So spec-parsed metadata cannot carry the key — an object declaring it fails to
|
|
177
|
+
load rather than loading with the rule silently dropped. Keeping a renderer-side
|
|
178
|
+
`requiredWhen ?? conditionalRequired` would have re-created exactly the second
|
|
179
|
+
de-facto contract the tombstone exists to prevent: the key would have kept
|
|
180
|
+
working in the UI while being rejected everywhere else, hiding the producer's bug
|
|
181
|
+
(AGENTS.md #0.1). "Backend-agnostic" (#1) does not argue for keeping it either —
|
|
182
|
+
`conditionalRequired` is an ObjectStack-spec-ism, so the only producers that ever
|
|
183
|
+
emit it are ObjectStack producers on ≤16, and the spec ships them a converter.
|
|
184
|
+
|
|
185
|
+
Removed from:
|
|
186
|
+
|
|
187
|
+
| package | site |
|
|
188
|
+
| :----------------------- | :-------------------------------------------------------------------------------------------------------- |
|
|
189
|
+
| `@object-ui/types` | the `conditionalRequired?:` member on `FormField` |
|
|
190
|
+
| `@object-ui/core` | the `??` fallback + rules-param member in `resolveFieldRuleState` |
|
|
191
|
+
| `@object-ui/components` | three pass-throughs in the form renderer |
|
|
192
|
+
| `@object-ui/plugin-form` | `ObjectForm`, `ModalForm`, `sectionFields`, `deriveMasterDetail` (×2) |
|
|
193
|
+
| `@object-ui/app-shell` | the field inspector's legacy read/auto-migrate, and the key's entry in `clientValidation`'s CEL lint list |
|
|
194
|
+
|
|
195
|
+
**Studio authors lose nothing.** The object designer's draft validation parses
|
|
196
|
+
against the spec's own `ObjectSchema`, so a draft carrying the key now surfaces
|
|
197
|
+
the tombstone's rename prescription under the same `fields.<name>.conditionalRequired`
|
|
198
|
+
path the CEL lint used to report — a better message than the inspector's silent
|
|
199
|
+
auto-migration, and one the server agrees with. That behavior is pinned by a test.
|
|
200
|
+
|
|
201
|
+
**Migrating:** rename the key to `requiredWhen` (the CEL value is unchanged), or
|
|
202
|
+
run `os migrate meta --from 16`.
|
|
203
|
+
|
|
204
|
+
- 02aef0c: fix(sdui): a `kind:'html'` page can use lazily-registered blocks, and recovers when one registers late
|
|
205
|
+
|
|
206
|
+
objectui#2953 had a twin one tier over, unreported. The whitelist a
|
|
207
|
+
`kind:'html'` page's source compiles against was built from `getAllTypes()` +
|
|
208
|
+
`getConfig()` — both loaded-only — so any block registered via `registerLazy()`
|
|
209
|
+
was rejected as _"not an allowed component"_.
|
|
210
|
+
|
|
211
|
+
The blast radius is worse than the react tier's. There, a missing block cost one
|
|
212
|
+
identifier; here a compile diagnostic fails the **whole page**, so a single
|
|
213
|
+
`<object-kanban>` replaced the entire page with `HTML page failed to compile (2)`.
|
|
214
|
+
And it never recovered: `layoutElement` was memoised on `[schema, pageType]` with
|
|
215
|
+
no registry signal, so the cached error panel outlived the plugin actually
|
|
216
|
+
landing — permanently broken for the session.
|
|
217
|
+
|
|
218
|
+
`ComponentRegistry` gains three lazy-aware reads:
|
|
219
|
+
|
|
220
|
+
- `getKnownTypes()` — loaded registrations **plus** pending lazy stubs, deduped.
|
|
221
|
+
The set a whitelist or manifest should be built from. `getAllTypes()` keeps its
|
|
222
|
+
loaded-only meaning ("what can render right now") and now says so.
|
|
223
|
+
- `getMeta(type, namespace?)` — metadata from the loaded registration, else from
|
|
224
|
+
a pending stub. `getConfig()` stays loaded-only, since callers read
|
|
225
|
+
`.component` off it.
|
|
226
|
+
- `getVersion()` — monotonic counter of changes to the known set, bumped on
|
|
227
|
+
register / unregister / registerLazy. A cache key that a type _count_ cannot
|
|
228
|
+
substitute for: one registration plus one unregistration leaves the count
|
|
229
|
+
untouched while the set changed.
|
|
230
|
+
|
|
231
|
+
`getJsxManifest()` builds from those, and `PageRenderer` subscribes to the
|
|
232
|
+
registry so a page that could not compile retries when the registry grows.
|
|
233
|
+
|
|
234
|
+
A stub carries no `inputs` yet, so its props surface as `unknown-prop` warnings
|
|
235
|
+
rather than errors — the page compiles and renders, and the inner
|
|
236
|
+
`SchemaRenderer` triggers the loader and swaps in the real block. Authoring-time
|
|
237
|
+
prop validation is unaffected: `sdui.manifest.json` is generated with every
|
|
238
|
+
plugin eagerly loaded, and asserts as much.
|
|
239
|
+
|
|
240
|
+
- c4db402: refactor(views): ListView's `aria` and `sharing` are the spec sub-shapes (#2890 scope A step 5)
|
|
241
|
+
|
|
242
|
+
Last rename batch in the ListView vocabulary migration.
|
|
243
|
+
|
|
244
|
+
**`aria`** is now the spec's `AriaPropsSchema`: `label` → `ariaLabel`,
|
|
245
|
+
`describedBy` → `ariaDescribedBy`, folded at the ListView boundary like every
|
|
246
|
+
other legacy key. Two things fall out of adopting the spec shape:
|
|
247
|
+
|
|
248
|
+
- `role` becomes authorable. The list region hardcoded `role="region"`; it now
|
|
249
|
+
reads `aria.role` and falls back to `region`.
|
|
250
|
+
- `aria.live` stays as a documented local extension — it has no spec
|
|
251
|
+
counterpart, and dropping it would silently disable a shipped capability.
|
|
252
|
+
Promote it rather than growing that extension.
|
|
253
|
+
|
|
254
|
+
**`sharing`** is now the spec's `ViewSharingSchema` (`{ type, lockedBy }`),
|
|
255
|
+
imported by reference — the local four-key object is gone. The legacy pair folds
|
|
256
|
+
in: `visibility` collapses onto the two ownership kinds the spec models (only
|
|
257
|
+
`private` is `personal`; `team` / `organization` / `public` are all
|
|
258
|
+
`collaborative`), and a bare `enabled: true` maps to `personal`, which is the
|
|
259
|
+
badge the user already saw (the old title fell back to `'private'`).
|
|
260
|
+
|
|
261
|
+
_Visible change_: the share badge's tooltip shows the spec ownership type, so a
|
|
262
|
+
view authored with `visibility: 'team'` reads "Sharing: collaborative" instead
|
|
263
|
+
of "Sharing: team". The four-value audience has no spec home and nothing but
|
|
264
|
+
that tooltip consumed it; keeping a second audience enum alive would re-open the
|
|
265
|
+
fork this issue closes.
|
|
266
|
+
|
|
267
|
+
Also fixes the **spec bridge**, which was doing the opposite of its job: given a
|
|
268
|
+
spec-shaped `sharing`, `transformListView` _downgraded_ it — inventing a legacy
|
|
269
|
+
`visibility` audience and an `enabled` flag that the renderer then had to fold
|
|
270
|
+
back. Both sides speak `ViewSharing` now, so it passes through.
|
|
271
|
+
|
|
272
|
+
`conditionalFormatting` and `exportOptions` are deliberately **not** folded.
|
|
273
|
+
Both objectui shapes are supersets carrying capability the spec cannot express —
|
|
274
|
+
the `{ field, operator, value }` rule form, and `maxRecords` / `includeHeaders`
|
|
275
|
+
/ `fileNamePrefix`. Folding them onto the narrower spec shapes would delete
|
|
276
|
+
working features; they want promotion upstream, not a rename.
|
|
277
|
+
|
|
278
|
+
- 5319bf1: feat(views): the list toolbar speaks one vocabulary — `userActions` (#2890 scope A step 3)
|
|
279
|
+
|
|
280
|
+
The seven bare `show*` toolbar flags fold into the spec's `userActions`, and the
|
|
281
|
+
renderer reads nothing else. `showDescription` folds into
|
|
282
|
+
`appearance.showDescription` at the same boundary.
|
|
283
|
+
|
|
284
|
+
| legacy | canonical |
|
|
285
|
+
| :-------------------------------------------------------- | :-------------------------------------------------------- |
|
|
286
|
+
| `showSearch` / `showSort` / `showFilters` / `showDensity` | `userActions.search` / `.sort` / `.filter` / `.rowHeight` |
|
|
287
|
+
| `showGroup` / `showHideFields` / `showColor` | `userActions.group` / `.hideFields` / `.rowColor` |
|
|
288
|
+
| `showDescription` | `appearance.showDescription` |
|
|
289
|
+
|
|
290
|
+
**The last three are new keys, and they close a capability hole rather than just
|
|
291
|
+
renaming one.** `@objectstack/spec`'s `UserActionsConfigSchema` documents itself
|
|
292
|
+
as "which interactive actions are available to users in the view toolbar — each
|
|
293
|
+
boolean toggles the corresponding toolbar element on/off", and already carries
|
|
294
|
+
`rowHeight` (objectui's `showDensity` under its spec name). Grouping, column
|
|
295
|
+
visibility and row coloring are the same kind of toggle: the spec models all
|
|
296
|
+
three as _configuration_ (`grouping`, `hiddenFields`, `rowColor`) but has no
|
|
297
|
+
"may the user change it" switch for any of them.
|
|
298
|
+
|
|
299
|
+
The consequence was visible in the product. With no `userActions` key to read,
|
|
300
|
+
the two list surfaces **hardcoded opposite policies**: `InterfaceListPage` (the
|
|
301
|
+
author-curated interface page) pinned all three OFF, `ObjectDataPage` pinned two
|
|
302
|
+
ON — and an interface-page author could not turn grouping back on for end users
|
|
303
|
+
at all. Both surfaces now express their policy as `userActions` defaults, which
|
|
304
|
+
an author can override.
|
|
305
|
+
|
|
306
|
+
Until the keys land in `@objectstack/spec`, `@object-ui/types` carries them as a
|
|
307
|
+
documented `.extend()` on `UserActionsConfigSchema` (the same shape
|
|
308
|
+
`ListColumnSchema` uses while waiting on objectstack#3761); it collapses into a
|
|
309
|
+
plain re-export once they do. Note the spec schema is not `.strict()`, so before
|
|
310
|
+
this an author writing `userActions: { group: false }` had it **silently
|
|
311
|
+
stripped** — valid on parse, no effect at render.
|
|
312
|
+
|
|
313
|
+
Defaults are unchanged and deliberately asymmetric, matching what these flags
|
|
314
|
+
have always done: `search` / `sort` / `filter` / `rowHeight` / `group` are on
|
|
315
|
+
unless turned off; `hideFields` / `rowColor` are off unless turned on. Making
|
|
316
|
+
them uniform would grow two buttons on every existing view, so it is left as its
|
|
317
|
+
own product decision rather than smuggled into a vocabulary migration.
|
|
318
|
+
|
|
319
|
+
Also drops a dead relay in app-shell's `ObjectView`, which forwarded
|
|
320
|
+
`showDescription` onto the node although `ListView` has only ever read
|
|
321
|
+
`appearance.showDescription`.
|
|
322
|
+
|
|
323
|
+
- f59f2c1: refactor(actions): `navigation` becomes a named alias of the spec's `url`, sharing one navigator (#2944)
|
|
324
|
+
|
|
325
|
+
The last open item of #2944: `ActionRunner` dispatched a seventh action type,
|
|
326
|
+
`navigation`, that `@objectstack/spec`'s `ActionType` does not contain. The issue
|
|
327
|
+
asked for a decision — promote it upstream or delete the case. Neither, as stated.
|
|
328
|
+
|
|
329
|
+
- **Promoting it is wrong.** The spec already has `url` for "go to a location",
|
|
330
|
+
with `openIn` for the new-tab/same-tab choice. A seventh type would put a
|
|
331
|
+
second spec name on one operation, which is the exact failure the #2901 audit
|
|
332
|
+
is named after: _a second definition of the vocabulary exists, and the renderer
|
|
333
|
+
is faithful to the wrong one_.
|
|
334
|
+
- **Deleting it is worse, because it is silent.** `{ type: 'navigation', to: … }`
|
|
335
|
+
is authored today (`element:button` CTAs). Without the case the action falls
|
|
336
|
+
through to `executeActionSchema`, which returns `{ success: true }` — a green
|
|
337
|
+
toast that navigates nowhere. That is #2960's trap.
|
|
338
|
+
|
|
339
|
+
So it stays, but stops being dialect. `ObjectUiLocalActionType` /
|
|
340
|
+
`OBJECTUI_LOCAL_ACTION_TYPES` in `@object-ui/types` declare it as objectui's own
|
|
341
|
+
alias of `url` — the same treatment #2985 gave `PageVisualizationAlias` — and the
|
|
342
|
+
runner routes both names through one navigator.
|
|
343
|
+
|
|
344
|
+
**The alias had already drifted, which is the point.** `executeNavigation` was
|
|
345
|
+
quietly the weaker of the two implementations: no `${param.X}` / `${ctx.X}`
|
|
346
|
+
interpolation, `openIn` ignored, and no `/api/…` full-page short-circuit (the
|
|
347
|
+
redirect-dance case `url` exists to handle). An author who wrote
|
|
348
|
+
`{ type: 'navigation', to: '/x?p=${param.p}' }` shipped the literal `${param.p}`,
|
|
349
|
+
while the identical `url` action resolved it. Both names now behave identically;
|
|
350
|
+
`url` in turn gains `replace` pass-through, the one modifier only the alias had.
|
|
351
|
+
|
|
352
|
+
Additive only. `replace` is omitted from the `NavigationHandler` options object
|
|
353
|
+
when unset, so hosts see the option shape they already saw.
|
|
354
|
+
|
|
355
|
+
The new guard is structural rather than another assertion. The runner's built-in
|
|
356
|
+
dispatch is a table typed `Record<RunnableActionType, …>` instead of a `switch`,
|
|
357
|
+
so an `ActionType` the spec **adds** stops compiling until an executor exists for
|
|
358
|
+
it — the Tier-2 "validates at save, renders nothing at run time" failure (#2942)
|
|
359
|
+
becomes a build error for actions. `spec-derived-unions.test.ts` additionally
|
|
360
|
+
asserts `navigation` is _absent_ from the spec enum, so the day it is adopted
|
|
361
|
+
upstream, the test fails and names the alias to retire.
|
|
362
|
+
|
|
363
|
+
- ce08d55: chore(deps): upgrade `@objectstack/*` to 17.0.0-rc.0, and let the spec take back what it now owns
|
|
364
|
+
|
|
365
|
+
`spec` / `client` / `formula` / `lint` move from `^16.x` to `^17.0.0-rc.0`. Two
|
|
366
|
+
groups of v17 changes reach this repo, and they pull in opposite directions —
|
|
367
|
+
the spec pruned surface objectui re-exported, and adopted surface objectui had
|
|
368
|
+
been carrying locally.
|
|
369
|
+
|
|
370
|
+
**The spec pruned dead Theme config (objectstack#3494), so the re-exports went
|
|
371
|
+
with it.** `ThemeSchema` dropped `spacing`, `breakpoints`, `logo`, `density`,
|
|
372
|
+
`wcagContrast`, `rtl`, `touchTarget` and `keyboardNavigation` — authorable but
|
|
373
|
+
never enforced, so authoring them was already a silent no-op. `@object-ui/types`
|
|
374
|
+
re-exported those sub-schemas _by reference_ (issue #2231), so they could not
|
|
375
|
+
survive the prune without becoming hand-written mirrors — exactly the second
|
|
376
|
+
de-facto contract AGENTS.md #0.1 forbids. Removed from the public surface:
|
|
377
|
+
|
|
378
|
+
- Types: `Spacing`, `Breakpoints`, `DensityMode`, `WcagContrastLevel`,
|
|
379
|
+
`ThemeLogo`, and the deprecated `SpacingScale` alias
|
|
380
|
+
- Schemas: `SpacingSchema`, `SpacingScaleSchema`, `BreakpointsSchema`,
|
|
381
|
+
`ThemeLogoSchema`, and the `SpacingSchemaType` / `BreakpointsSchemaType` helpers
|
|
382
|
+
- `Theme.spacing`, `Theme.breakpoints` and `Theme.logo`
|
|
383
|
+
|
|
384
|
+
`mergeThemes` no longer merges the three dropped keys. `generateThemeVars` is
|
|
385
|
+
unaffected — it never emitted them, which is why the liveness audit called them
|
|
386
|
+
dead. The one real consumer was `ThemeProvider`, which set the favicon from
|
|
387
|
+
`theme.logo.favicon`; that path is gone, because v17 strips the key at parse and
|
|
388
|
+
it could never arrive again. The live favicon is unaffected: it comes from
|
|
389
|
+
operator branding (`getFaviconUrl()`), applied in the console's `index.html`,
|
|
390
|
+
`main.tsx`, and on route change.
|
|
391
|
+
|
|
392
|
+
Nothing else read the pruned types. In particular the list-density feature is
|
|
393
|
+
untouched — `useDensityMode` and `rowHeightToDensityMode` use `@object-ui/core`'s
|
|
394
|
+
own local `DensityMode`, which never came from the spec.
|
|
395
|
+
|
|
396
|
+
**The spec adopted objectui's ListColumn extensions (objectui#2231), so the
|
|
397
|
+
extension collapsed.** `ListColumnSchema` used to `.extend()` the spec with two
|
|
398
|
+
fields, each carrying a note to promote it upstream rather than grow the
|
|
399
|
+
extension; v17 did exactly that. `summary` is now the spec's
|
|
400
|
+
`union([ColumnSummarySchema, ColumnSummaryConfigSchema])` — the same enum ∪
|
|
401
|
+
`{ type, field }` form `useColumnSummary` reads — and `prefix` is the spec's
|
|
402
|
+
`ColumnPrefixSchema`. `ListColumnSchema` is now a plain by-reference re-export.
|
|
403
|
+
One behavior change rides along: `prefix.type` defaults to `'text'` on parse
|
|
404
|
+
instead of staying `undefined`, so the cell renderer always gets a value.
|
|
405
|
+
|
|
406
|
+
**Node 22 is now the floor.** Every `@objectstack` package declares
|
|
407
|
+
`engines.node: ">=22.0.0"` (objectstack#3825; Node 20 reached EOL 2026-04-30).
|
|
408
|
+
This repo claimed `>=20` and ran CI on Node 20.x, so it promised — and validated
|
|
409
|
+
— a runtime its own core dependency does not support. `engines.node` is now
|
|
410
|
+
`>=22`, CI runs Node 22.x, and the CI/deployment docs say so.
|
|
411
|
+
|
|
412
|
+
The major stays 17: per AGENTS.md the major tracks `@objectstack`'s major, which
|
|
413
|
+
is also 17, and that convention deliberately outranks semver purity — so the
|
|
414
|
+
removals above ship as a minor rather than desyncing the two.
|
|
415
|
+
|
|
416
|
+
- 390c071: feat(record): declare inputs for the seven configurable record:\* blocks, and curate six
|
|
417
|
+
|
|
418
|
+
Seven `record:*` blocks shipped with renderers that read props but declared no
|
|
419
|
+
`inputs`. That combination is the worst of both: the renderer honours
|
|
420
|
+
`limit`, `severity`, `location` …, while every authoring surface — the designer
|
|
421
|
+
panel, the AI vocabulary, the generated manifest — reports the block takes no
|
|
422
|
+
configuration. objectui#3013 recorded them as deliberately uncurated for
|
|
423
|
+
exactly that reason.
|
|
424
|
+
|
|
425
|
+
The declarations mirror what each renderer actually reads:
|
|
426
|
+
|
|
427
|
+
| block | inputs |
|
|
428
|
+
| -------------------------------------- | ---------------------------------------------------------------------------- |
|
|
429
|
+
| `record:activity` | 11 — from `RecordActivityComponentProps` |
|
|
430
|
+
| `record:chatter` / `record:discussion` | 5 — from `RecordChatterComponentProps` |
|
|
431
|
+
| `record:alert` | 8 — severity, title, body, visible, icon, action, dismissible, dismissKey |
|
|
432
|
+
| `record:quick_actions` | 7 — actionNames, requiredPermissions, location, align, inline, variant, size |
|
|
433
|
+
| `record:history` | 3 — limit, emptyText, unknownUserText |
|
|
434
|
+
| `record:reference_rail` | 1 — hideEmpty |
|
|
435
|
+
|
|
436
|
+
`inputs` describe what an AUTHOR writes, which is a subset of what the renderer
|
|
437
|
+
reads. `entries`, `loading` and resolved `actions` are injected by the host
|
|
438
|
+
shell off RecordContext; declaring them would invite a model to hand-write the
|
|
439
|
+
data the page is supposed to fetch. `aria` is omitted for the reason it is
|
|
440
|
+
omitted on `record:details` — an accessibility escape hatch, not a layout
|
|
441
|
+
choice. `location` takes its enum from the spec's `ACTION_LOCATIONS` rather
|
|
442
|
+
than restating it, per objectui#3019.
|
|
443
|
+
|
|
444
|
+
Six of the seven are now in `PUBLIC_BLOCKS`: configurable and absent from the
|
|
445
|
+
contract is the state objectui#3006 was about. The contract goes 36 → 42 tags,
|
|
446
|
+
all resolving.
|
|
447
|
+
|
|
448
|
+
`record:chatter` stays out — it is the same renderer as `record:discussion`
|
|
449
|
+
under a Salesforce-familiar name, kept for schemas already in the wild. Two
|
|
450
|
+
spellings of one block is ambiguity an authoring model cannot resolve, so the
|
|
451
|
+
vocabulary carries the spec's name. A test compares the two input lists, so the
|
|
452
|
+
day they diverge the exclusion stops being justified and fails.
|
|
453
|
+
|
|
454
|
+
A companion assertion requires every curated `record:*` tag to declare inputs.
|
|
455
|
+
A curated tag with none reads as "takes no configuration" when the renderer in
|
|
456
|
+
fact reads props — the same gap objectui#3006 opened, pointed the other way.
|
|
457
|
+
|
|
458
|
+
- 912496d: feat(types,core): the `*Validation` rule types derive from spec 17, and the engine agrees with the server — objectstack#4115
|
|
459
|
+
|
|
460
|
+
The five spec-named rule variants in `data-protocol.ts` were hand-written
|
|
461
|
+
interfaces, each labelled `(ObjectStack Spec v2.0.1)` while the installed spec
|
|
462
|
+
was `17.0.0-rc.0`. Nothing bound them to the spec, so fifteen majors of drift
|
|
463
|
+
accumulated with `tsc` silent throughout and the comment still vouching for it.
|
|
464
|
+
They are now `z.input` derivations of `ScriptValidationSchema` /
|
|
465
|
+
`StateMachineValidationSchema` / `CrossFieldValidationSchema` /
|
|
466
|
+
`ConditionalValidationSchema` / `FormatValidationSchema`, and canonicity is
|
|
467
|
+
carried by that binding plus a parity gate rather than by a comment (#3017).
|
|
468
|
+
|
|
469
|
+
`z.input`, not `z.infer`, because objectui consumes **authored** metadata as it
|
|
470
|
+
arrives over `/meta` — before the spec applies its defaults and canonicalizes
|
|
471
|
+
expressions. That is the shape actually in the JSON.
|
|
472
|
+
|
|
473
|
+
**Breaking, in the shape of the rule types** (minor per this repo's version
|
|
474
|
+
policy — see AGENTS.md §9):
|
|
475
|
+
|
|
476
|
+
| | was | is |
|
|
477
|
+
| --------------------------------- | ------------------------------------------- | ------------------------------------------------------- |
|
|
478
|
+
| `ConditionalValidation` | `condition` + `rules[]` | `when` + `then` / `otherwise` |
|
|
479
|
+
| `FormatValidation` | `pattern` + `flags`, 8 named formats | `regex`, the 4 formats the server implements |
|
|
480
|
+
| `Script`/`CrossField` `condition` | `string` | `string \| { dialect, source }` |
|
|
481
|
+
| `StateMachineValidation` | — | gains `initialStates` (objectstack#3165) |
|
|
482
|
+
| `BaseValidation` | no `priority`, `events` included `'delete'` | gains `priority`; `'delete'` retired (objectstack#3184) |
|
|
483
|
+
|
|
484
|
+
`UniquenessValidation` / `AsyncValidation` / `RangeValidation` are now
|
|
485
|
+
`@deprecated`. They have no spec counterpart — the spec removed the first two
|
|
486
|
+
deliberately (uniqueness → a unique index, since SELECT-then-INSERT is racy;
|
|
487
|
+
async → the form layer) — and the spec's `ValidationRuleSchema` rejects all
|
|
488
|
+
three, so no rule in those shapes can ride in `ObjectSchema.validations`.
|
|
489
|
+
|
|
490
|
+
**`ObjectValidationEngine` now agrees with `objectql`'s rule-validator.** It is a
|
|
491
|
+
client PRE-CHECK of rules the server enforces, so every disagreement cost the
|
|
492
|
+
user something real. Fixed:
|
|
493
|
+
|
|
494
|
+
- **Polarity was inverted.** The server violates a rule when the predicate is
|
|
495
|
+
TRUE; the engine violated it when the predicate was FALSE. Every
|
|
496
|
+
spec-authored `script` / `cross_field` rule produced the opposite verdict.
|
|
497
|
+
- **Envelope conditions were a silent no-op.** `{ dialect, source }` reached
|
|
498
|
+
`expression.trim()`, threw, was caught, and read as "passes".
|
|
499
|
+
- **`conditional` was a silent no-op**, reading `rule.condition` / `rule.rules`
|
|
500
|
+
where the spec says `when` / `then`; `otherwise` was never evaluated at all.
|
|
501
|
+
- **`format` produced FALSE REJECTIONS** — it read `rule.pattern`, and
|
|
502
|
+
`undefined.test(...)` threw into a catch that reported a violation, blocking
|
|
503
|
+
writes the server accepts.
|
|
504
|
+
- **An absent `active` disabled the rule** and an absent `events` threw; both
|
|
505
|
+
arrive absent from `/meta` because the spec defaults them at parse time.
|
|
506
|
+
- `priority` now orders execution; `initialStates` is enforced on insert;
|
|
507
|
+
`format`/`state_machine` only fire when the write touches the field; a broken
|
|
508
|
+
predicate or an uncompilable `regex` fails OPEN with a warning; and a rule type
|
|
509
|
+
the engine cannot evaluate (the spec's `json_schema`) warns instead of
|
|
510
|
+
reporting the record as valid.
|
|
511
|
+
|
|
512
|
+
The default `SimpleExpressionEvaluator` is not CEL and never was; it now binds
|
|
513
|
+
both the spec's `record.x` scope and objectui's historical bare `x`, and
|
|
514
|
+
documents that richer predicates need a CEL-backed evaluator. `validateRecord`'s
|
|
515
|
+
`event` parameter no longer accepts `'delete'`.
|
|
516
|
+
|
|
517
|
+
Gates: `packages/types/src/__tests__/validation-rule-spec-parity.test.ts` (key
|
|
518
|
+
sets, wire shapes, the pinned `then`/`otherwise` divergence with an inverted pin
|
|
519
|
+
that fails when objectstack#4171 is fixed upstream) and the rewritten engine
|
|
520
|
+
suite. objectstack#4115's ledger drops 120 → 115.
|
|
521
|
+
|
|
522
|
+
### Patch Changes
|
|
523
|
+
|
|
524
|
+
- 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.**
|
|
525
|
+
|
|
526
|
+
Three public types said something different from what the platform accepts. All
|
|
527
|
+
three divergences were found by making `packages/types`' tests compile (#3009)
|
|
528
|
+
and then resolving the declared `p1-spec-alignment.test.ts` debt site-by-site
|
|
529
|
+
instead of papering over it.
|
|
530
|
+
|
|
531
|
+
**`ActionParam` is now the authoring shape, aligned with the spec's input.**
|
|
532
|
+
`name` / `label` / `type` become optional and `field` / `objectOverride` appear:
|
|
533
|
+
the spec's primary way to declare a param — a bare field reference that inherits
|
|
534
|
+
label/type/validation/options from an object field — was unrepresentable while
|
|
535
|
+
all three were required. The _resolved_ shape the dialog consumes (after
|
|
536
|
+
app-shell's `resolveActionParams()` inlines the reference) remains
|
|
537
|
+
`@object-ui/core`'s `ActionParamDef`, with all three required. Authoring and
|
|
538
|
+
resolved are different types on purpose. `label` and option labels take the
|
|
539
|
+
spec's `I18nLabel` by import — which the new compile-time guard promptly
|
|
540
|
+
revealed to be aliased to plain `string` in the current spec (the per-locale
|
|
541
|
+
record is the separate `I18nObject`), so this is not a behavioural widening
|
|
542
|
+
today; importing the alias means objectui tracks any future widening
|
|
543
|
+
automatically.
|
|
544
|
+
|
|
545
|
+
**Breaking:** code destructuring `param.name` / `param.label` / `param.type` as
|
|
546
|
+
guaranteed must now handle the field-backed form (or consume the resolved
|
|
547
|
+
`ActionParamDef` instead, which is what dialog-side code should be doing).
|
|
548
|
+
|
|
549
|
+
**`ListViewInferred` is `z.input`, not `z.infer`.** The spec sub-schemas that
|
|
550
|
+
flow into the list-view surface (`userActions`, `tabs` → `ViewTab`, `sharing`)
|
|
551
|
+
carry `.default()`s, so the inferred output type made fields like
|
|
552
|
+
`userActions.refresh` or a tab's `pinned`/`visible` _required_ — but nothing on
|
|
553
|
+
the render path ever runs `.parse()`: `normalizeListViewSchema` deliberately
|
|
554
|
+
applies no defaults ("an absent flag stays absent", its own suite). The output
|
|
555
|
+
type therefore rejected valid authored metadata (`userActions: { sort: true }`)
|
|
556
|
+
while promising renderers defaults that never arrive. Typing the surface as
|
|
557
|
+
input matches both the author and the runtime object. Code that _trusted_ those
|
|
558
|
+
phantom defaults now gets an optionality error — which is a latent bug surfacing,
|
|
559
|
+
not a regression: the value really could be absent.
|
|
560
|
+
|
|
561
|
+
**`FormField.dependsOn` is `DependsOnInput`.** The runtime reader
|
|
562
|
+
(`resolveCascadingOptions`) has always accepted a bare name, a list of names, or
|
|
563
|
+
lookup-parameter entries `{ field, param }` — its parameter type says so. The
|
|
564
|
+
public property said `string`, so array-authored metadata type-errored while
|
|
565
|
+
working, and the form renderer read the key through `(f as any).dependsOn` to
|
|
566
|
+
get past its own type. The shape now lives in `@object-ui/types` (single source
|
|
567
|
+
of truth next to `FormField`), `@object-ui/core` imports and re-exports it, and
|
|
568
|
+
the two `as any` reads in the components form renderer are typed.
|
|
569
|
+
|
|
570
|
+
**The `p1-spec-alignment.test.ts` exclusion is gone.** Its 14 errors resolved:
|
|
571
|
+
the two "sharing in ObjectUI format" tests and the legacy-ARIA-spelling fixture
|
|
572
|
+
are deleted/rewritten — those dialects are _normalizer input_, folded by
|
|
573
|
+
`normalizeListViewSchema` and asserted branch-by-branch in core's
|
|
574
|
+
`normalize-list-view.test.ts`, the seam where the fold actually runs; asserting
|
|
575
|
+
them on the canonical type only ever "passed" because nothing compiled the file.
|
|
576
|
+
One fixture claimed a shape no surface ever admitted (an ObjectQL triplet as a
|
|
577
|
+
spec `ViewTab.filter`) and was corrected to the rule-object form. Every test
|
|
578
|
+
file in `@object-ui/types` is now compiled, with no exclusions.
|
|
579
|
+
|
|
580
|
+
Discrimination-checked: reverting `ListViewInferred` to `z.infer`, `dependsOn`
|
|
581
|
+
to `string`, or `ActionParam.name` to required each produces the expected
|
|
582
|
+
compile error in the now-compiled test files (`TS2739` / `TS2322` / `TS2741`);
|
|
583
|
+
restored, all projects are clean.
|
|
584
|
+
|
|
585
|
+
- 95b7214: fix(list,grid,detail,tree,core): every column resolver reads one key (#3104 PR2)
|
|
586
|
+
|
|
587
|
+
PR1 (#3119) put a canonicalizing fold at ListView's ingestion boundary. This
|
|
588
|
+
converges the 22 read sites themselves onto `columnIdentity()` from
|
|
589
|
+
`@object-ui/core`, so a surface that is NOT downstream of that fold resolves
|
|
590
|
+
the same identity anyway.
|
|
591
|
+
|
|
592
|
+
That distinction is the user-visible part. A standalone `object-grid` node —
|
|
593
|
+
authored directly on a page, with no `list-view` above it — never passed
|
|
594
|
+
through `normalizeListViewSchema`. Its `getSelectFields` read `c.field` alone
|
|
595
|
+
while the `ensureId` probe one line above read `f?.name || f?.field`, so a
|
|
596
|
+
legacy `{ name: 'account' }` column reached `$select` as a literal `undefined`
|
|
597
|
+
hole: the server never returned the field and every cell in that column came
|
|
598
|
+
back empty. Same for `ObjectTree`, `RelatedList` and the `record:details` /
|
|
599
|
+
`record:related_list` renderers.
|
|
600
|
+
|
|
601
|
+
Converged:
|
|
602
|
+
|
|
603
|
+
| Surface | Was | Now |
|
|
604
|
+
| ---------------------------------------- | ---------------------------------------------- | ----------------------------------- |
|
|
605
|
+
| `ListView` ×9 + its 2 request builders | `name \|\| fieldName \|\| field` vs `f?.field` | `columnIdentity()` |
|
|
606
|
+
| `RelatedList` ×8 | `accessorKey \|\| field \|\| name` | `accessorKey \|\| columnIdentity()` |
|
|
607
|
+
| `ObjectGrid` | name-first probe vs `c.field` projection | `columnIdentity()` |
|
|
608
|
+
| `ObjectTree` | `name \|\| fieldName \|\| field \|\| key` | `columnIdentity() \|\| key` |
|
|
609
|
+
| `buildExpandFields` | `field ?? name ?? fieldName` | `columnIdentity()` |
|
|
610
|
+
| `record-details` / `record-related-list` | `field \|\| name (\|\| key)` | `columnIdentity() (\|\| key)` |
|
|
611
|
+
|
|
612
|
+
`accessorKey` keeps its precedence in `RelatedList` — it is TanStack Table's
|
|
613
|
+
column key, not ObjectStack metadata identity, and only the `field || name`
|
|
614
|
+
tail was converged. `key` stays a tail fallback in `ObjectTree` and
|
|
615
|
+
`record-related-list` for the same reason: it is a generic entry key.
|
|
616
|
+
|
|
617
|
+
Two incidental fixes that TypeScript surfaced once the resolver stopped
|
|
618
|
+
returning `any`: ListView's filter-field options and its hide-fields popover
|
|
619
|
+
both built entries keyed `undefined` for a column with no resolvable identity.
|
|
620
|
+
Those entries could never match a column; they are now dropped.
|
|
621
|
+
|
|
622
|
+
**Inventory re-triage.** PR1 recorded 24 family members. Two were mis-classified
|
|
623
|
+
and are reclassified here rather than converged — reading what they actually
|
|
624
|
+
feed shows they are not column reads at all:
|
|
625
|
+
|
|
626
|
+
- `ViewPreview.tsx` adapts a ViewItem **form** section to what `object-form`
|
|
627
|
+
selects by (`field` → `name`) — the #3090 two-layer join.
|
|
628
|
+
- `SchemaForm.tsx` renders an arbitrary metadata **array** into a popover
|
|
629
|
+
summary and guesses at a display key; the entries are validations, actions,
|
|
630
|
+
or whatever the JSON schema declares.
|
|
631
|
+
|
|
632
|
+
So the family was 22, and it is now **0**. The ratchet asserts that, asserts
|
|
633
|
+
each converged surface actually routes through the shared reader (a surface
|
|
634
|
+
that dropped identity resolution instead of converging it goes red), and pins
|
|
635
|
+
`accessorKey`'s precedence in `RelatedList`.
|
|
636
|
+
|
|
637
|
+
- 7d9734d: feat(core): say which column identity key won, out loud (#3104 PR3)
|
|
638
|
+
|
|
639
|
+
Closes the battle opened in #3104. PR1 (#3119) put the canonicalizing fold at
|
|
640
|
+
ingestion; PR2 (#3122) converged all 22 read sites onto `columnIdentity()`.
|
|
641
|
+
This is the audible half.
|
|
642
|
+
|
|
643
|
+
A column carrying two identity keys that **disagree** — `{ field: 'account',
|
|
644
|
+
name: 'account_name' }` — now logs a one-time dev-mode warning naming which key
|
|
645
|
+
won and what to change:
|
|
646
|
+
|
|
647
|
+
```
|
|
648
|
+
[ObjectUI] Column carries two identities: `field: 'account'` and
|
|
649
|
+
`name: 'account_name'`. `field` wins — it is the only key `ListColumnSchema`
|
|
650
|
+
declares — and `name` has been rewritten to match, so the rendered column and
|
|
651
|
+
the requested field agree. Fix the producer: drop `name` and author `field`
|
|
652
|
+
only. (objectui#3104)
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
The fold making the two halves agree is what stops the bug, but silently
|
|
656
|
+
rewriting `name` to match `field` also hides that the producer is emitting a
|
|
657
|
+
contradiction. The renderer recovering is not the same as the metadata being
|
|
658
|
+
right, so the recovery says so.
|
|
659
|
+
|
|
660
|
+
Deliberately narrow:
|
|
661
|
+
|
|
662
|
+
- **Only contradictions.** A legacy-only column (`{ name: 'stage' }`) is legacy,
|
|
663
|
+
not conflicting — it is stamped without noise.
|
|
664
|
+
- **Warn once per (identity, conflicting spelling).** Columns are re-normalized
|
|
665
|
+
on every render; a warning that floods the console is a warning that gets
|
|
666
|
+
muted. Keyed by the pair rather than the identity alone, so a column carrying
|
|
667
|
+
two different stale spellings reports both — the author needs to fix every
|
|
668
|
+
producer, not just the first one seen.
|
|
669
|
+
- **Silent under `NODE_ENV=production`**, and the fold still runs there.
|
|
670
|
+
|
|
671
|
+
`resetColumnIdentityWarnings()` is exported for tests.
|
|
672
|
+
|
|
673
|
+
**No lint rule, and that is a measured decision.** #3104 asked for
|
|
674
|
+
`no-restricted-syntax` on `.field ?? .name` to be evaluated on its
|
|
675
|
+
false-positive rate first. With the family at zero, all 12 remaining scanner
|
|
676
|
+
hits are legitimate — a syntactic rule cannot tell a two-layer join from a dual
|
|
677
|
+
read, because the distinction is what the keys mean in that layer, not how the
|
|
678
|
+
expression is spelled. Adopting it would mean 12 inline disables on correct
|
|
679
|
+
code, which trains the next author to reach for the disable. The ratchet carries
|
|
680
|
+
a `verdict` and a `why` per site instead, so a new hit gets triaged rather than
|
|
681
|
+
silenced. The evaluation is written into the ratchet's header.
|
|
682
|
+
|
|
683
|
+
**Ledger item resolved with no change needed.** #3104 flagged `ListColumn` for
|
|
684
|
+
disposition under objectstack#4115 (spec-named symbols must be imports, not
|
|
685
|
+
declarations). `ListColumnSchema` is already a by-reference re-export of
|
|
686
|
+
`@objectstack/spec/ui`, and `spec-subschema-parity.test.ts` already pins it by
|
|
687
|
+
reference identity — the only check that distinguishes a re-export from a
|
|
688
|
+
faithful fork. Already compliant; nothing to do.
|
|
689
|
+
|
|
690
|
+
- aebfa4f: chore(core): deprecate `ObjectValidationEngine` — rule enforcement stays single-implementation on the server (#3110)
|
|
691
|
+
|
|
692
|
+
`ObjectValidationEngine` / `defaultObjectValidationEngine` / `validateRecord` are
|
|
693
|
+
now `@deprecated`. **Nothing is removed and no behaviour changes** — existing
|
|
694
|
+
callers keep working exactly as they did after #3103.
|
|
695
|
+
|
|
696
|
+
**Why.** Object-level validation rules are _enforcement_, and enforcement is
|
|
697
|
+
single-implementation on the server (`objectql`'s rule-validator). objectui
|
|
698
|
+
already draws that line for the predicates it _does_ evaluate client-side:
|
|
699
|
+
`evaluator/fieldRules.ts` handles the presentation predicates (`visibleWhen` /
|
|
700
|
+
`readonlyWhen` / `requiredWhen`) by delegating to the canonical
|
|
701
|
+
`ExpressionEngine`, "rather than re-implementing a parallel evaluator"
|
|
702
|
+
(ADR-0036). This engine was that parallel evaluator, on the enforcement side.
|
|
703
|
+
|
|
704
|
+
#3103 converged its semantics onto the server rule-for-rule, with eight
|
|
705
|
+
mutation-tested gates — and still left a known divergence: the server carries
|
|
706
|
+
ADR-0113's legacy-violation exemption (reject only when the merged state violates
|
|
707
|
+
_and_ this write makes it worse), which this engine does not implement. Editing
|
|
708
|
+
an unrelated field on a legacy row would be blocked here and accepted there. One
|
|
709
|
+
careful pass still left a gap, which is the argument: mirroring cross-repo
|
|
710
|
+
behaviour is structurally unreliable, not unreliable-this-time.
|
|
711
|
+
|
|
712
|
+
**What to use instead.** Let the write fail and render the server's rejection —
|
|
713
|
+
it is already structured (`field` / `code` / `message`, plus a label since
|
|
714
|
+
objectstack#3957). For pre-submit feedback, the answer is a validate-only
|
|
715
|
+
(dry-run) write on the server: identical UX, zero parity risk, and it covers the
|
|
716
|
+
two rule kinds a client can never check — `unique` (needs the database) and
|
|
717
|
+
`json_schema` (ajv lives server-side).
|
|
718
|
+
|
|
719
|
+
**The decision is a mechanism, not a comment.** What #3103 removed was a doc
|
|
720
|
+
comment claiming spec canonicity that had been false for fifteen majors;
|
|
721
|
+
shipping its successor as another comment would repeat the mistake one level up.
|
|
722
|
+
`validation-engine-stays-unwired.test.ts` scans `packages/*/src` and fails if a
|
|
723
|
+
production module starts referencing the engine, naming the file and the issue to
|
|
724
|
+
reverse first. Barrels still re-export it — publishing a deprecated API is not
|
|
725
|
+
wiring — and host applications are free to keep importing it.
|
|
726
|
+
|
|
727
|
+
The five spec-derived rule TYPES in `@object-ui/types` are unaffected: they are
|
|
728
|
+
the anchor for objectstack#4115's ledger and are independent of whether objectui
|
|
729
|
+
ships an engine.
|
|
730
|
+
|
|
731
|
+
- c4d7b20: fix(view,list,core): a view's filter no longer disappears, or arrives as a predicate on columns that don't exist
|
|
732
|
+
|
|
733
|
+
Sweeping the other `$filter` producers after #3078 turned up two live defects in
|
|
734
|
+
`ObjectView`, which fetches its own data for calendar / kanban / gallery /
|
|
735
|
+
timeline (grid delegates to `ObjectGrid`).
|
|
736
|
+
|
|
737
|
+
**1. An object filter was dropped, and only for non-grid views.**
|
|
738
|
+
`table.defaultFilters` is declared `Record<string, any>`, and the merge tested
|
|
739
|
+
`baseFilter.length > 0` — `undefined > 0` for an object. So the filter vanished
|
|
740
|
+
and the view returned **every record**. `ObjectGrid` assigns the same value
|
|
741
|
+
straight to `params.$filter`, so one view definition filtered correctly as a
|
|
742
|
+
grid and returned everything as a calendar.
|
|
743
|
+
|
|
744
|
+
**2. Rule objects were spread into the `and`, not wrapped.**
|
|
745
|
+
`['and', ...baseFilter, ...userFilter]` is only correct when the source is an
|
|
746
|
+
array of AST nodes. `activeView.filter` is a spec `ViewFilterRule[]`, so
|
|
747
|
+
spreading put bare rule objects where the AST expects nodes:
|
|
748
|
+
|
|
749
|
+
```js
|
|
750
|
+
isFilterAST([
|
|
751
|
+
"and",
|
|
752
|
+
{ field: "stage", operator: "eq", value: "won" },
|
|
753
|
+
["owner", "=", "me"],
|
|
754
|
+
]);
|
|
755
|
+
// false → 400 since objectstack#4121
|
|
756
|
+
parseFilterAST(same);
|
|
757
|
+
// {$and:[{field:'stage',operator:'eq',value:'won'}, {owner:'me'}]}
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
That second line is a predicate over three columns named `field`, `operator`
|
|
761
|
+
and `value` — which don't exist.
|
|
762
|
+
|
|
763
|
+
> **Correction.** The first version of this note said the spread was "reachable
|
|
764
|
+
> whenever a view with a filter meets a user filter value". That was wrong for
|
|
765
|
+
> `ObjectView`: the branch required a non-empty user filter, and nothing ever
|
|
766
|
+
> wrote the state it was built from, so it could never run. The shape is
|
|
767
|
+
> genuinely broken — a live server answers it with a 400 — and the adapter-level
|
|
768
|
+
> defence added alongside is still warranted for any producer that emits it, but
|
|
769
|
+
> **this particular site was dead code, not a live defect.** Defect 1 above was
|
|
770
|
+
> live: it sat on the always-taken path. The dead machinery behind the wrong
|
|
771
|
+
> claim is removed in a follow-up.
|
|
772
|
+
|
|
773
|
+
New in `@object-ui/core`: `toFilterNode` normalizes one source (rule array / AST
|
|
774
|
+
/ MongoDB object) and `mergeFilterNodes` combines sources as siblings under one
|
|
775
|
+
`and`. `ObjectView` and `ListView.buildEffectiveFilter` both use them, so the
|
|
776
|
+
three filter shapes are reconciled in one place instead of by hand at each
|
|
777
|
+
renderer.
|
|
778
|
+
|
|
779
|
+
`ObjectStackAdapter` also now translates a bare rule object sitting directly
|
|
780
|
+
under a logical node — the chokepoint defence for any producer still emitting
|
|
781
|
+
the spread shape. Only rule-_shaped_ objects are touched; a child with no
|
|
782
|
+
`field` is a genuine MongoDB condition and passes through untouched.
|
|
783
|
+
|
|
784
|
+
**Correcting a comment shipped in #3078.** `buildEffectiveFilter` documented the
|
|
785
|
+
dropped-object case as unreachable, "nothing in this repo produces one for a
|
|
786
|
+
list view". That was wrong: `ObjectView` passes `mergedFilters` straight into
|
|
787
|
+
that schema's `filter`, and its last fallback is `table.defaultFilters`. The
|
|
788
|
+
case is now handled rather than explained away.
|
|
789
|
+
|
|
790
|
+
Verified with 19 tests across the four packages; reverting each source file
|
|
791
|
+
fails the ones that cover it. Emitted filters are asserted against the spec's
|
|
792
|
+
own `isFilterAST` / `parseFilterAST`, including an executable pin on what the
|
|
793
|
+
old spread shape produced.
|
|
794
|
+
|
|
795
|
+
- 6f29aa5: fix(grid): a legacy string row action runs instead of green-toasting a no-op — objectui#2960
|
|
796
|
+
|
|
797
|
+
A list view declaring `rowActions: ['convert_lead']` rendered a menu item that
|
|
798
|
+
performed **zero network requests** and reported success. Where the object also
|
|
799
|
+
declared the same action with `locations: ['list_item']`, the row menu showed a
|
|
800
|
+
working entry and a dead duplicate of it side by side.
|
|
801
|
+
|
|
802
|
+
**The name never became an action.** `ObjectGrid` dispatched the legacy form as
|
|
803
|
+
`{ type: <action name>, params: { record } }` — the action _name_ landing in the
|
|
804
|
+
runner's `type` slot, never resolved against the object's action defs. It
|
|
805
|
+
matches no built-in type and (absent a handler registered under that exact name)
|
|
806
|
+
no handler either, so it fell through to `ActionRunner.executeActionSchema`,
|
|
807
|
+
which returned `{success: true, reload: true, close: true}` for a schema with
|
|
808
|
+
nothing in it. `handlePostExecution` then fired the green "Action completed
|
|
809
|
+
successfully" toast.
|
|
810
|
+
|
|
811
|
+
Two changes, either of which would have surfaced the bug:
|
|
812
|
+
|
|
813
|
+
**① `ObjectGrid` resolves legacy names against `objectDef.actions`.** A name
|
|
814
|
+
that matches a declared action is promoted to that def and dispatched through
|
|
815
|
+
the same path as a `list_item` action — so it actually runs, and it picks up the
|
|
816
|
+
def's label, `visible`/`disabled` predicates, param dialog and capability gate,
|
|
817
|
+
none of which the string form could carry. A name that matches an action already
|
|
818
|
+
rendered as a def is dropped, which is what removes the dead twin. Names that
|
|
819
|
+
resolve to nothing are still dispatched by name, since a consumer may have
|
|
820
|
+
registered a runner handler under exactly that name.
|
|
821
|
+
|
|
822
|
+
**② `ActionRunner`'s empty-schema fallthrough fails loudly.** It no longer
|
|
823
|
+
reports success for an action it never ran: a dispatch with no registered
|
|
824
|
+
handler and no `api`/`endpoint`/`navigate`/`redirect`/`onClick` returns a
|
|
825
|
+
failure naming the action. Schema-only shapes that _do_ declare something — a
|
|
826
|
+
bare `redirect`, an explicit `reload`/`close` — run exactly as before.
|
|
827
|
+
|
|
828
|
+
- ad0183a: fix(data-objectstack,core): an object filter no longer depends on whether the query expands a lookup
|
|
829
|
+
|
|
830
|
+
#3072 single-sourced the ARRAY branch of the adapter's two `find()` routes. The
|
|
831
|
+
object branch was left as it was: `convertQueryParams` converted a MongoDB-style
|
|
832
|
+
filter to AST while `translateFilterToAST` returned it verbatim — so the same
|
|
833
|
+
`$filter` went out in two formats, decided by whether the query happened to
|
|
834
|
+
expand a lookup.
|
|
835
|
+
|
|
836
|
+
Measured across 21 operator shapes, **four diverged**. Most of the gap turned
|
|
837
|
+
out to be harmless — `{$and: […]}` survives the plain route as a
|
|
838
|
+
`['$and','=',[…]]` comparison that `parseFilterAST` reads back as a real `$and`,
|
|
839
|
+
and `$exists` vs `$null` is a difference the server treats identically. Two were
|
|
840
|
+
not harmless:
|
|
841
|
+
|
|
842
|
+
- **The unknown-operator guard only ran on one route.** `convertFiltersToAST`
|
|
843
|
+
throws on an unrecognised operator, with a comment saying it does so "to avoid
|
|
844
|
+
silent failure" — but the expanded route never called it, so a typo'd operator
|
|
845
|
+
threw on a plain read and shipped silently whenever a lookup was expanded.
|
|
846
|
+
- **`$regex` was silently rewritten to `contains`.** `$regex: 'a.c'` matches
|
|
847
|
+
"abc"; `contains 'a.c'` matches only those three literal characters. That is a
|
|
848
|
+
_different question_, not a weaker version of the same one, and neither result
|
|
849
|
+
looks wrong on screen. The rewrite sat behind a `console.warn`, which is not
|
|
850
|
+
an error channel in a deployed app — and the function's own unknown-operator
|
|
851
|
+
message never listed `$regex` among the supported set. The spec has no
|
|
852
|
+
`$regex` (`FILTER_OPERATORS`, `data/filter.zod.ts`), so there is nothing to
|
|
853
|
+
translate it into: it is now refused, the same treatment the neighbouring
|
|
854
|
+
unknown operator already got. Nothing in the repo depended on the conversion.
|
|
855
|
+
|
|
856
|
+
Both refusals now throw `FilterOperatorError`, carrying `code: 'INVALID_FILTER'`
|
|
857
|
+
/ `httpStatus: 400`. The pre-existing unknown-operator throw was a bare `Error`,
|
|
858
|
+
which `classifyLoadError` classifies as a network fault — so a malformed filter
|
|
859
|
+
told the user to check their connection (#3066), the one thing it definitely
|
|
860
|
+
was not.
|
|
861
|
+
|
|
862
|
+
- aa1240a: fix(sdui): lazily-registered public blocks reach a `kind:'react'` page's scope, and ReactRunner keeps the errors it catches
|
|
863
|
+
|
|
864
|
+
Two defects in the trusted `kind:'react'` page tier.
|
|
865
|
+
|
|
866
|
+
**objectui#2953 — the contract skipped lazy blocks.** `getPublicConfigs()`
|
|
867
|
+
resolved every curated `PUBLIC_BLOCKS` tag through `getConfig()`, which reads
|
|
868
|
+
loaded registrations only, so a block registered with `registerLazy()` was
|
|
869
|
+
absent from the contract until its plugin chunk happened to be imported. In
|
|
870
|
+
`apps/console` that silently dropped `object-kanban`, `object-calendar`,
|
|
871
|
+
`object-gantt`, `object-timeline`, `object-map` and `markdown` from every react
|
|
872
|
+
page's scope — writing `<ObjectKanban/>` threw `ReferenceError` even though the
|
|
873
|
+
tag is a first-class contract member, and whether it threw depended on load
|
|
874
|
+
order. `getPublicConfigs()` now resolves pending lazy stubs too, returning them
|
|
875
|
+
with `lazy: true` and no `component` (new `PublicComponentConfig` type); the
|
|
876
|
+
injected wrapper renders through `SchemaRenderer`, which triggers the loader and
|
|
877
|
+
shows its placeholder. `getConfig()` stays loaded-only by design.
|
|
878
|
+
|
|
879
|
+
**objectui#2954 — ReactRunner discarded its own error state.**
|
|
880
|
+
`getDerivedStateFromProps` re-transpiled and re-evaluated the page source on
|
|
881
|
+
every render and unconditionally set `error: null`. React runs it before the
|
|
882
|
+
re-render that follows `getDerivedStateFromError`, so the boundary threw away
|
|
883
|
+
the error it had just caught, rebuilt an identical throwing element, and the
|
|
884
|
+
throw escaped past its own `fallback` to the renderer's generic panel; `onError`
|
|
885
|
+
was gated on state that had already been cleared and never fired for a
|
|
886
|
+
compile-time error at all; and each compile minted a fresh page function — a new
|
|
887
|
+
element type — that remounted the subtree and wiped the page's `useState`. The
|
|
888
|
+
transpile+eval is now memoised on `(code, scope)`, errors persist until the
|
|
889
|
+
inputs actually change, and `onError` reports each error exactly once.
|
|
890
|
+
|
|
891
|
+
- d10f526: fix(sdui): the curated contract lists `record:line_items`, the tag that actually resolves
|
|
892
|
+
|
|
893
|
+
`PUBLIC_BLOCKS` carried `line_items` — the bare tag. `@object-ui/plugin-form`
|
|
894
|
+
registers the block as `record:line_items` with `skipFallback: true`, which
|
|
895
|
+
exists precisely so the bare name is _not_ claimed, so that key never existed
|
|
896
|
+
and the curated entry could never resolve. Its four siblings in the list are all
|
|
897
|
+
`record:`-prefixed, and plugin-form's own comment says "Register
|
|
898
|
+
record:line_items"; the bare spelling was a slip.
|
|
899
|
+
|
|
900
|
+
The effect was a block that has shipped all along — a full renderer, a label,
|
|
901
|
+
five declared `inputs` — being absent from the public contract, from the JSX
|
|
902
|
+
type surface, from the generated manifest, and from every `kind:'react'` page's
|
|
903
|
+
scope. It read as an unimplemented aspirational entry, which is how it was
|
|
904
|
+
recorded when objectui#2979 added the contract-coverage guard.
|
|
905
|
+
|
|
906
|
+
With the tag corrected the contract has no gaps left: all 36 curated tags
|
|
907
|
+
resolve in the console, `record:line_items` among them with its full `inputs`.
|
|
908
|
+
The guard's known-unimplemented list is now empty and stays asserted, so the
|
|
909
|
+
next entry that cannot resolve surfaces instead of being explained away.
|
|
910
|
+
|
|
911
|
+
- 2d5d594: fix(list,detail): sorting a lookup column no longer orders by an invisible key — #3096
|
|
912
|
+
|
|
913
|
+
A relational column (`lookup` / `master_detail` / `user` / `tree`) never holds
|
|
914
|
+
the string its cell shows: it holds the `$expand`-ed record, or a raw foreign-key
|
|
915
|
+
id whose label was resolved separately. Every sort path took that raw value as
|
|
916
|
+
its key, so the column of names came back in an order with no relation to the
|
|
917
|
+
names — sorting looked broken, with nothing saying the key was something else.
|
|
918
|
+
|
|
919
|
+
The two halves are fixed differently, because they can order by different things:
|
|
920
|
+
|
|
921
|
+
- **Client-side sorts** (grid column headers, any `data-table`, a non-windowed
|
|
922
|
+
related list) now key off the label the cell renders, via the new
|
|
923
|
+
`getSortValue` / `compareSortValues` in `@object-ui/core` — which resolves an
|
|
924
|
+
expanded record through `getRecordDisplayName` (ADR-0079), so the sort key and
|
|
925
|
+
the lookup cell agree on which field names a record. This replaces two broken
|
|
926
|
+
comparators: `a[col] < b[col]` is always false between two objects (the
|
|
927
|
+
comparator collapsed to a constant and permuted the rows), and
|
|
928
|
+
`String(a[col])` is `"[object Object]"` (every row compared equal, so the sort
|
|
929
|
+
silently did nothing).
|
|
930
|
+
- **Server `$orderby` sorts** cannot be fixed here — the key is the stored id by
|
|
931
|
+
construction, and `objectstack#4256` settled that no relation join is coming.
|
|
932
|
+
So those entry points stop offering the illusion: the ListView toolbar sort
|
|
933
|
+
picker withholds relational fields and explains why (pointing at a formula
|
|
934
|
+
field as the supported way to sort by a related name), and a windowed related
|
|
935
|
+
list renders no sort button for them.
|
|
936
|
+
|
|
937
|
+
A relational field the view's CURRENT sort already uses stays listed, labelled
|
|
938
|
+
`(by ID)`, so view metadata authored or saved with such a sort round-trips
|
|
939
|
+
instead of rendering a blank row and losing the sort on the next edit.
|
|
940
|
+
|
|
941
|
+
- 7f23cd0: fix(form): a numeric/boolean select option survives selection with its type intact — #3090
|
|
942
|
+
|
|
943
|
+
`SelectOptionSchema.value` has accepted `string | number | boolean` for as
|
|
944
|
+
long as it has existed, but the Radix controls underneath speak strings:
|
|
945
|
+
picking `{ value: 2 }` silently submitted `"2"` — a wrong-typed write into a
|
|
946
|
+
number field that nothing on the client ever reported. (Display half-worked:
|
|
947
|
+
a numeric default matched its numeric item; only SELECTION morphed the type.)
|
|
948
|
+
|
|
949
|
+
The renderers now stringify on the way into the control and map the selection
|
|
950
|
+
back to the AUTHORED option value on the way out (`matchOptionValue`), across
|
|
951
|
+
the in-form select, the standalone `type: 'select'` component, and the
|
|
952
|
+
standalone `type: 'radio-group'` component. The TS types stop lying to match:
|
|
953
|
+
`SelectOption.value` / `RadioOption.value` and the corresponding
|
|
954
|
+
`value`/`defaultValue`/`onChange` channels widen to what the zod schemas
|
|
955
|
+
always accepted — a call site treating `option.value` as `string` is now a
|
|
956
|
+
compile error pointing at a real latent crash, not a false comfort.
|
|
957
|
+
|
|
958
|
+
The ripple the widening named, handled at each boundary: `@object-ui/core`'s
|
|
959
|
+
`OptionLike.value` widens (the option engines compare by identity, so values
|
|
960
|
+
flow opaquely; the option-lint's CEL-literal domain stringifies at its
|
|
961
|
+
boundary), and the multi-value field widgets (checkboxes / multiselect /
|
|
962
|
+
radio) stringify at theirs — multi-value fields store string arrays.
|
|
963
|
+
|
|
964
|
+
Round-trip pinned by real Radix interactions in jsdom: the in-form select
|
|
965
|
+
submits `2` (number), the standalone select hands its handler `false`
|
|
966
|
+
(boolean).
|
|
967
|
+
|
|
968
|
+
- Updated dependencies [9e7349e]
|
|
969
|
+
- Updated dependencies [8864971]
|
|
970
|
+
- Updated dependencies [b41f401]
|
|
971
|
+
- Updated dependencies [19e9fa0]
|
|
972
|
+
- Updated dependencies [38ca8be]
|
|
973
|
+
- Updated dependencies [4952edf]
|
|
974
|
+
- Updated dependencies [7f0252e]
|
|
975
|
+
- Updated dependencies [7639a61]
|
|
976
|
+
- Updated dependencies [94e63ef]
|
|
977
|
+
- Updated dependencies [c4db402]
|
|
978
|
+
- Updated dependencies [5319bf1]
|
|
979
|
+
- Updated dependencies [49e5671]
|
|
980
|
+
- Updated dependencies [b5b97e2]
|
|
981
|
+
- Updated dependencies [f59f2c1]
|
|
982
|
+
- Updated dependencies [4874117]
|
|
983
|
+
- Updated dependencies [ce08d55]
|
|
984
|
+
- Updated dependencies [2374a49]
|
|
985
|
+
- Updated dependencies [ea7f477]
|
|
986
|
+
- Updated dependencies [7f23cd0]
|
|
987
|
+
- Updated dependencies [24e0e0a]
|
|
988
|
+
- Updated dependencies [3a6cf24]
|
|
989
|
+
- Updated dependencies [aa35561]
|
|
990
|
+
- Updated dependencies [03bd53b]
|
|
991
|
+
- Updated dependencies [3c1f321]
|
|
992
|
+
- Updated dependencies [a045a32]
|
|
993
|
+
- Updated dependencies [912496d]
|
|
994
|
+
- Updated dependencies [9867281]
|
|
995
|
+
- @object-ui/types@17.1.0
|
|
996
|
+
|
|
997
|
+
## 17.0.0
|
|
998
|
+
|
|
999
|
+
### Minor Changes
|
|
1000
|
+
|
|
1001
|
+
- f9bbddb: feat: gate detail/form edit & delete on the server's effective operation set (#3546)
|
|
1002
|
+
|
|
1003
|
+
PR-4 (#3391) wired the **list/toolbar** surface (ObjectView Import, ListView /
|
|
1004
|
+
ObjectGrid Export) to the server-resolved effective API operation set
|
|
1005
|
+
(`/me/permissions` `apiOperations`, intersected via
|
|
1006
|
+
`resolveCrudAffordances(obj, effectiveApiOperations?)`). The **detail / form**
|
|
1007
|
+
surfaces still gated edit/delete on the bucket + `userActions` alone. This
|
|
1008
|
+
extends the same intersection to them, so the record page and its forms never
|
|
1009
|
+
offer an operation the server would 405.
|
|
1010
|
+
|
|
1011
|
+
- **core** `isObjectInlineEditable(obj, effectiveApiOperations?)` gains the same
|
|
1012
|
+
optional second argument as `resolveCrudAffordances` — inline-edit is now
|
|
1013
|
+
additionally ANDed with the server allowing `update`.
|
|
1014
|
+
- **app-shell** `RecordDetailView` threads the object's effective operations into
|
|
1015
|
+
the synthesized Edit/Delete header actions and the record-body inline-edit
|
|
1016
|
+
gate (`canEdit`); `RelatedRecordActionsBridge` intersects each **child**
|
|
1017
|
+
object's Create/Edit/Delete handlers with that child's own effective set.
|
|
1018
|
+
- **plugin-detail** `record:details` ANDs its inline-edit affordance with the
|
|
1019
|
+
object's effective `update`.
|
|
1020
|
+
- **plugin-form** `ObjectForm`'s blanket managed-object field lock also engages
|
|
1021
|
+
when the server denies `update` (edit mode) / `create` (create mode).
|
|
1022
|
+
|
|
1023
|
+
Backward-compatible: a missing effective set (unrestricted object, older
|
|
1024
|
+
backend, or no `PermissionProvider`) leaves the resolved affordance untouched —
|
|
1025
|
+
the bucket/`userActions` decision wins, exactly as today. Layers on top of the
|
|
1026
|
+
existing per-object `check('edit')` / `check('delete')` permission gates
|
|
1027
|
+
(intersection, never union).
|
|
1028
|
+
|
|
1029
|
+
- 2735de6: feat: render the server's effective API operation set (#3391 PR-4)
|
|
1030
|
+
|
|
1031
|
+
The frontend now consumes the per-object **effective API operation set** the
|
|
1032
|
+
server resolves (from `/me/permissions` `apiOperations`, framework #3391) —
|
|
1033
|
+
never the raw `apiMethods` — so Import/Export/New/Edit/Delete buttons match what
|
|
1034
|
+
the server will actually admit, and a 405 import refusal shows a dedicated
|
|
1035
|
+
message instead of silently falling back.
|
|
1036
|
+
|
|
1037
|
+
- **core** `resolveCrudAffordances(obj, effectiveApiOperations?)` — new optional
|
|
1038
|
+
second argument intersects each affordance bit with its API operation
|
|
1039
|
+
(create/import→create/import, edit→update, delete→delete, exportCsv→export).
|
|
1040
|
+
Omitting it (old backend / no effective set) leaves affordances unchanged.
|
|
1041
|
+
- **permissions** — `/me/permissions` response carries per-object
|
|
1042
|
+
`apiOperations`; `PermissionContextValue.getObjectApiOperations(object)`
|
|
1043
|
+
exposes it (undefined when absent → callers keep current behavior); `check()`
|
|
1044
|
+
maps `import→allowCreate`, `export→allowRead`.
|
|
1045
|
+
- **app-shell** `ObjectView` intersects its toolbar affordances with the object's
|
|
1046
|
+
effective operations (Import); the platform-admin identity-import bypass is
|
|
1047
|
+
unaffected.
|
|
1048
|
+
- **plugin-list** `ListView` / **plugin-grid** `ObjectGrid` gate the Export
|
|
1049
|
+
button (and export handler) on effective `export`; `plugin-grid` gains the
|
|
1050
|
+
`@object-ui/permissions` workspace dependency.
|
|
1051
|
+
- **plugin-grid** `ImportWizard` — a 405 / `OBJECT_API_METHOD_NOT_ALLOWED`
|
|
1052
|
+
import refusal is detected by a new `isImportNotAllowed` predicate at every
|
|
1053
|
+
catch site (async, sync, dry-run) and STOPS with a dedicated
|
|
1054
|
+
`grid.import.notAllowed` message (10 locales + fallback dict) — it never falls
|
|
1055
|
+
back to the sync/legacy path (which 405s too), distinct from the 404
|
|
1056
|
+
route-absent fallback.
|
|
1057
|
+
|
|
1058
|
+
Backward-compatible: a missing effective set (unrestricted object, older
|
|
1059
|
+
backend, or no permission provider) preserves the current default-allow
|
|
1060
|
+
behavior everywhere.
|
|
1061
|
+
|
|
1062
|
+
- cd09a7b: refactor(views): ListView reads the spec-canonical `columns`, with legacy `fields` folded in one normalizer (#2890 scope A step 1)
|
|
1063
|
+
|
|
1064
|
+
`ListViewSchema` has been derived from `@objectstack/spec/ui` since #2231, but
|
|
1065
|
+
the renderer still spoke objectui's own vocabulary for the same concepts. First
|
|
1066
|
+
rename closed: **`fields` → `columns`**.
|
|
1067
|
+
|
|
1068
|
+
Legacy acceptance does not disappear — stored view metadata in user databases
|
|
1069
|
+
carries `fields` — but it now lives in exactly one place instead of being
|
|
1070
|
+
re-implemented per read-site:
|
|
1071
|
+
|
|
1072
|
+
- **New `normalizeListViewSchema` (`@object-ui/core`)** folds `fields` into
|
|
1073
|
+
`columns` (canonical wins when both are present) and drops the legacy key, so
|
|
1074
|
+
a read-site that was missed fails loudly instead of quietly taking the legacy
|
|
1075
|
+
path. It also absorbs the `viewType` renderability default ListView applied
|
|
1076
|
+
inline. Non-mutating, idempotent, and returns its input by reference when
|
|
1077
|
+
there is nothing to fold, so ListView's downstream memos keep a stable
|
|
1078
|
+
dependency identity.
|
|
1079
|
+
- **`ListView` normalizes once at the component boundary**, before anything
|
|
1080
|
+
reads the schema. This is what guarantees the fold runs: nothing on the render
|
|
1081
|
+
path parses view metadata through zod (the zod schemas serve the CLI
|
|
1082
|
+
validator, the VS Code extension and tests), so a `z.preprocess` on
|
|
1083
|
+
`ListViewSchema` — spec-side or local — would never execute.
|
|
1084
|
+
- **Producers emit `columns`**: `ObjectView`'s `renderListView` payload,
|
|
1085
|
+
`ObjectDataPage`, `InterfaceListPage` and the `list-view` registry defaults
|
|
1086
|
+
had been _downgrading_ already-canonical `columns` config back to `fields`.
|
|
1087
|
+
|
|
1088
|
+
Two latent inconsistencies go away with it: the filter builder's
|
|
1089
|
+
objectDef-not-loaded fallback now resolves `ListColumn.field` (it read only
|
|
1090
|
+
`name`/`fieldName`, so object-form columns produced unnamed filter entries), and
|
|
1091
|
+
the column list no longer depends on which of the two keys a host happened to
|
|
1092
|
+
emit.
|
|
1093
|
+
|
|
1094
|
+
`fields` stays declared on `ListViewSchema` and in the drift guard's sanctioned
|
|
1095
|
+
set — it is still valid input, and `@objectstack/spec`'s `react-blocks.ts`
|
|
1096
|
+
sanctions it as the React-tier `<ListView fields>` prop — but it is input-only.
|
|
1097
|
+
|
|
1098
|
+
- f1abf0e: fix(views): ListView reads the spec-canonical `filter`, so a view's base filter reaches every visualization (#2890 scope A step 4)
|
|
1099
|
+
|
|
1100
|
+
Third rename in the ListView vocabulary migration: **`filters` → `filter`**. Unlike
|
|
1101
|
+
the first two this closes a live bug, because the fork was asymmetric.
|
|
1102
|
+
|
|
1103
|
+
`ListView` was the **only** surface in the repo reading `filters`. Every child
|
|
1104
|
+
view — `ObjectGrid`, `ObjectGallery`, `ObjectKanban`, `ObjectCalendar`,
|
|
1105
|
+
`ObjectGantt`, `ObjectMap`, `ObjectTree`, `ObjectChart` — reads `filter`, and
|
|
1106
|
+
`ListView` handed them `filters`. Wherever a child fetches its own rows instead
|
|
1107
|
+
of receiving `ListView`'s, the view's base filter was silently dropped:
|
|
1108
|
+
|
|
1109
|
+
- **a `chart` list view aggregated the whole object.** The chart branch built an
|
|
1110
|
+
`object-chart` node with `filters:`; `ObjectChart` reads `schema.filter` and
|
|
1111
|
+
never read `filters`, so a chart view with a base filter charted unfiltered
|
|
1112
|
+
totals.
|
|
1113
|
+
- the same applied to any of the other view components rendered standalone from
|
|
1114
|
+
a list-view-shaped config.
|
|
1115
|
+
|
|
1116
|
+
Conversely, a **spec-authored** list view — one carrying `filter`, which is what
|
|
1117
|
+
the spec says and what `runtime-metadata-persistence` and "Save as view" already
|
|
1118
|
+
persist — rendered **unfiltered** in `ListView`, because nothing read that key.
|
|
1119
|
+
|
|
1120
|
+
The fold is a key rename only. Both keys carry an ObjectQL FilterNode array
|
|
1121
|
+
everywhere in objectui; every consumer passes the value straight to `$filter`.
|
|
1122
|
+
(The spec types `filter` as `ViewFilterRule[]` — `{field, operator, value}`
|
|
1123
|
+
objects — so objectui's field is typed from the spec but used as something else.
|
|
1124
|
+
That mismatch is real and left alone here: converting formats inside a
|
|
1125
|
+
vocabulary fold would change what reaches the data source.)
|
|
1126
|
+
|
|
1127
|
+
Also collapses a duplicated computation in `app-shell`'s `ObjectView`, which
|
|
1128
|
+
computed the same effective filter **twice** — once as `filter` for the child
|
|
1129
|
+
views, once as `filters` for `ListView` — with the two copies subtly different
|
|
1130
|
+
(only one fell back to `listSchema.filter`; only the other ran token
|
|
1131
|
+
substitution over the URL filters). There is now one computation, keeping both
|
|
1132
|
+
behaviors.
|
|
1133
|
+
|
|
1134
|
+
`filters` stays declared on `ListViewSchema` and in the drift guard's sanctioned
|
|
1135
|
+
set — stored views carry it and it is still valid input — but it is input-only.
|
|
1136
|
+
|
|
1137
|
+
- f05b84e: refactor(views): ListView resolves density from the spec-canonical `rowHeight` (#2890 scope A step 2)
|
|
1138
|
+
|
|
1139
|
+
Second rename in the ListView vocabulary migration: **`densityMode` → `rowHeight`**,
|
|
1140
|
+
folded in the same `normalizeListViewSchema` that step 1 introduced.
|
|
1141
|
+
|
|
1142
|
+
Unlike `fields`/`columns` this is not a pure alias — the two vocabularies are
|
|
1143
|
+
different sizes. The spec has five row heights (`compact`/`short`/`medium`/
|
|
1144
|
+
`tall`/`extra_tall`); ListView's toolbar offers three densities
|
|
1145
|
+
(`compact`/`comfortable`/`spacious`). Both directions now live in one place as
|
|
1146
|
+
`DENSITY_MODE_TO_ROW_HEIGHT` / `ROW_HEIGHT_TO_DENSITY_MODE`, chosen so a fold
|
|
1147
|
+
followed by a read is a round trip (`spacious` → `tall` → `spacious`), with the
|
|
1148
|
+
narrowing collapse (`short` → `compact`, `extra_tall` → `spacious`) stated once
|
|
1149
|
+
instead of being re-derived per call site.
|
|
1150
|
+
|
|
1151
|
+
Two behavior fixes fall out of it:
|
|
1152
|
+
|
|
1153
|
+
- **Precedence is no longer inverted.** `ListView` read `densityMode` _first_, so
|
|
1154
|
+
a view carrying both keys rendered the legacy value — backwards from every
|
|
1155
|
+
other legacy/canonical pair in the schema. The canonical key now wins.
|
|
1156
|
+
- **The toolbar stops re-seeding the legacy key.** `ObjectView`'s
|
|
1157
|
+
`onDensityChange` persisted `densityMode` into stored view metadata on every
|
|
1158
|
+
density toggle, so the legacy vocabulary kept regrowing underneath the
|
|
1159
|
+
migration. It persists `rowHeight` now.
|
|
1160
|
+
|
|
1161
|
+
`densityMode` stays declared on `ListViewSchema` and in the drift guard's
|
|
1162
|
+
sanctioned set — stored views carry it and it is still valid input — but it is
|
|
1163
|
+
input-only.
|
|
1164
|
+
|
|
1165
|
+
### Patch Changes
|
|
1166
|
+
|
|
1167
|
+
- 7b35e4b: fix(dashboard,charts): resolve `{current_user_id}` in widget filters (framework #3574)
|
|
1168
|
+
|
|
1169
|
+
A dashboard widget filtered on `{current_user_id}` rendered `0`. The token
|
|
1170
|
+
reached SQL as a literal, matched no row, and nothing was logged on the client
|
|
1171
|
+
or the server — a silent zero that reads as "you have no work" rather than
|
|
1172
|
+
"this filter did not resolve". The same token in a list-view filter resolved
|
|
1173
|
+
correctly, so a user-scoped list and a user-scoped widget over the same data
|
|
1174
|
+
disagreed.
|
|
1175
|
+
|
|
1176
|
+
There was no shared resolver. Three ad-hoc implementations had grown up
|
|
1177
|
+
independently — `ObjectView` for list views, `ObjectDataPage` for URL filter
|
|
1178
|
+
triples, `NavigationRenderer` for hrefs — and each understood only the filter
|
|
1179
|
+
shape its own surface used. `ObjectView`'s opened with
|
|
1180
|
+
`if (!Array.isArray(filter)) return filter`, so it could not have been reused
|
|
1181
|
+
by dashboard widgets even in principle: widget filters are MongoDB-style
|
|
1182
|
+
objects. Widgets therefore got no resolution at all — `DatasetWidget` called
|
|
1183
|
+
`resolveDateMacros` and nothing else, which is why `{today}` worked in a widget
|
|
1184
|
+
and `{current_user_id}` silently did not.
|
|
1185
|
+
|
|
1186
|
+
- **`@object-ui/core`** — new `utils/filter-tokens.ts` with
|
|
1187
|
+
`resolveContextTokens` and `resolveFilterPlaceholders`. The latter expands
|
|
1188
|
+
_every_ placeholder vocabulary in one call and is what surfaces should use;
|
|
1189
|
+
resolving only some of them is the whole defect. The walk handles arrays and
|
|
1190
|
+
plain objects uniformly, so one resolver covers both platform filter shapes.
|
|
1191
|
+
- **`@object-ui/react`** — new `FilterScopeProvider` / `useFilterScope`. The
|
|
1192
|
+
renderer packages deliberately do not depend on `@object-ui/auth`, so the
|
|
1193
|
+
shell supplies the session values. This is a separate context from
|
|
1194
|
+
`PredicateScopeContext`, which is the expression evaluation scope and carries
|
|
1195
|
+
no organization.
|
|
1196
|
+
- **`@object-ui/plugin-dashboard` / `@object-ui/plugin-charts`** — all six
|
|
1197
|
+
widgets that previously resolved date macros only now resolve both
|
|
1198
|
+
vocabularies: `DatasetWidget`, `ObjectMetricWidget`, `ObjectDataTable`,
|
|
1199
|
+
`ObjectPivotTable`, and `ObjectChart` (dataset-bound and inline paths). The
|
|
1200
|
+
chart's `compareTo` comparison filter gets the session pass too — otherwise
|
|
1201
|
+
the overlay series silently ignored the owner clause the primary series
|
|
1202
|
+
honoured.
|
|
1203
|
+
- **`@object-ui/app-shell`** — `ObjectView`'s local `substituteFilterTokens`
|
|
1204
|
+
and `ObjectDataPage`'s inline `=== '{current_user_id}'` ternary now delegate
|
|
1205
|
+
to the shared resolver, so both also gain `{current_org_id}` and date macros.
|
|
1206
|
+
Two of the three ad-hoc implementations are gone rather than joined by a
|
|
1207
|
+
fourth.
|
|
1208
|
+
|
|
1209
|
+
An unresolvable token is left intact rather than dropped: leaving it yields an
|
|
1210
|
+
empty result, whereas dropping the clause would _widen_ the result set and show
|
|
1211
|
+
a signed-out viewer everyone's data. It is no longer silent — the resolver
|
|
1212
|
+
warns, naming the token, and suggests the intended spelling for known
|
|
1213
|
+
near-misses (`{current_user}`, `{user_id}`, `{organization_id}`). Authoring-time
|
|
1214
|
+
enforcement lands separately as `filter-token-unknown` in `@objectstack/lint`.
|
|
1215
|
+
|
|
1216
|
+
- e16ed2d: fix(dashboard,charts): send widget `dateGranularity`/`sortBy`/`limit` to the query, and give funnels a real stage order (framework#3588)
|
|
1217
|
+
|
|
1218
|
+
`DatasetWidget` never read `widget.options`. Four keys an author writes there
|
|
1219
|
+
change the query the server compiles, so a widget declaring
|
|
1220
|
+
`options: { dateGranularity: 'month' }` grouped by the raw timestamp and drew
|
|
1221
|
+
one bar per record, and `sortBy`/`sortOrder` produced no ordering at all.
|
|
1222
|
+
|
|
1223
|
+
- `DatasetWidget` lowers `options.dateGranularity`, `options.sortBy` +
|
|
1224
|
+
`options.sortOrder`, and `options.limit` into the `DatasetSelection` it posts.
|
|
1225
|
+
A `sortBy` naming something the widget does not project is dropped rather than
|
|
1226
|
+
sent, so a stale sort key left by an edit degrades to "unordered" instead of
|
|
1227
|
+
failing the widget against the server's stricter validation. These keys also
|
|
1228
|
+
join the refetch signature, so editing one in the designer refetches instead
|
|
1229
|
+
of re-rendering the previous grid.
|
|
1230
|
+
- Funnel stages follow a **declared order**. `AdvancedChartImpl` sorted funnel
|
|
1231
|
+
segments by value descending, unconditionally — which overrode any server
|
|
1232
|
+
ordering and rendered a sales pipeline as a tidy narrowing shape whatever the
|
|
1233
|
+
stages' real sequence, hiding the very anomaly (a bulge at Proposal) the chart
|
|
1234
|
+
exists to show. It now honours a `categoryOrder`, which `DatasetWidget`
|
|
1235
|
+
derives from the dimension field's picklist option order — the pipeline order
|
|
1236
|
+
an author already declared on the object — or from an explicit
|
|
1237
|
+
`options.stageOrder`. With no declared order the value-descending default is
|
|
1238
|
+
unchanged, and a category missing from the order is kept (after the declared
|
|
1239
|
+
ones), never dropped.
|
|
1240
|
+
- New `@object-ui/core` helpers `buildCategoryOrder` / `buildCategoryRank`,
|
|
1241
|
+
keyed by both the stored value and the display label like the existing
|
|
1242
|
+
`buildOptionColorMap`, so ordering works whether or not the server resolved
|
|
1243
|
+
the dimension's labels.
|
|
1244
|
+
|
|
1245
|
+
Requires the framework-side fix in objectstack#3588 for the selection keys to
|
|
1246
|
+
take effect server-side.
|
|
1247
|
+
|
|
1248
|
+
- 53642d4: fix(core,fields): a string `$orderby` is a clause, not a character array — and localize the sharing-rule widgets (objectstack#3821)
|
|
1249
|
+
|
|
1250
|
+
**The recipient picker listed nothing, ever.** `QueryParams['$orderby']` was
|
|
1251
|
+
typed as `Record | string[] | SortObject[]`, so `queryParamsToRecord` sent any
|
|
1252
|
+
non-array value through `Object.entries`. Handed the clause string `'name asc'`
|
|
1253
|
+
— which callers do build by hand — it walked the string index by index and
|
|
1254
|
+
emitted `$orderby=0 n,1 a,2 m,3 e,4 ,5 a,6 s,7 c`. The server sorted by columns
|
|
1255
|
+
that don't exist and every row was filtered out, so
|
|
1256
|
+
`sys_sharing_rule.recipient_id` rendered "No matches" for every recipient type
|
|
1257
|
+
and no sharing rule could be created from the Console. `ObjectGrid` builds the
|
|
1258
|
+
same shape from a schema-level `sort` in three places, so grids with a string
|
|
1259
|
+
sort silently showed an empty table.
|
|
1260
|
+
|
|
1261
|
+
A string `$orderby` is now passed through verbatim (the server's OData
|
|
1262
|
+
normalizer has always parsed `'name asc'`), and the type admits `string`.
|
|
1263
|
+
`RecipientPickerField` additionally switched to the structured
|
|
1264
|
+
`{ name: 'asc' }` form so it can't regress this way against any data source.
|
|
1265
|
+
|
|
1266
|
+
**The three sharing-rule authoring widgets never had translations.**
|
|
1267
|
+
`ObjectRefField`, `RecipientPickerField` and `FilterConditionField` hardcoded
|
|
1268
|
+
their English copy — a Chinese Console showed "Select an object", "Select a
|
|
1269
|
+
user", "Search…", "No matches", "Edit as JSON". They now go through
|
|
1270
|
+
`useFieldTranslation` like every other widget, with keys added under `fields.*`
|
|
1271
|
+
in all ten locales.
|
|
1272
|
+
|
|
1273
|
+
The recipient placeholder was the interesting one: it read
|
|
1274
|
+
`` `Select a ${recipientType.replace(/_/g,' ')}` ``, interpolating the enum
|
|
1275
|
+
value into an English sentence — a shape no locale can translate. It is now a
|
|
1276
|
+
per-type key (`fields.recipient.selectUser`, `…selectBusinessUnit`, …), so
|
|
1277
|
+
"选择业务单元" and "Select a business unit" no longer have to share a structure.
|
|
1278
|
+
|
|
1279
|
+
**Editing a rule silently dropped its recipient.** The picker resets the stored
|
|
1280
|
+
id when `recipient_type` changes, because an id valid for a user is meaningless
|
|
1281
|
+
for a team. It treated the edit form's `'' → 'user'` hydration as such a change:
|
|
1282
|
+
opening any saved rule blanked the recipient, and saving persisted the blank.
|
|
1283
|
+
Only a non-empty predecessor now counts as a type switch.
|
|
1284
|
+
|
|
1285
|
+
**Building a filter submitted the surrounding form.** None of `FilterBuilder`'s
|
|
1286
|
+
controls declared `type="button"`, and a bare `<button>` inside a `<form>`
|
|
1287
|
+
defaults to `type="submit"`. Adding, removing or clearing a condition therefore
|
|
1288
|
+
submitted the sharing-rule dialog — firing validation mid-edit, and on an
|
|
1289
|
+
already-valid form saving the record before the admin was done.
|
|
1290
|
+
|
|
1291
|
+
**A rejected write showed the user raw server diagnostics.** The form rendered
|
|
1292
|
+
`error.message` verbatim, so a sharing / RLS denial reached the dialog and the
|
|
1293
|
+
toast as `FORBIDDEN: insufficient privileges to update showcase_private_note
|
|
1294
|
+
pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and the
|
|
1295
|
+
record id to whoever hit it. Permission failures now render localized copy
|
|
1296
|
+
(`form.noPermissionToSave`, added in all ten locales), with the server text kept
|
|
1297
|
+
on the console for debugging; other failures still show the server's message,
|
|
1298
|
+
which is the useful part, and fall back to `form.submitFailed` when there is
|
|
1299
|
+
none — replacing the previously hardcoded English "An error occurred during
|
|
1300
|
+
submission".
|
|
1301
|
+
|
|
1302
|
+
**The detail header offered "Edit" on records the user may only read.** Object
|
|
1303
|
+
permissions can't express "this one record is read-only" — a read-only sharing
|
|
1304
|
+
grant sits inside an object the user may otherwise edit — so the header showed
|
|
1305
|
+
the primary Edit CTA, opened the form, and let the user retype a field before
|
|
1306
|
+
the server rejected the save. `DetailView` now gates Edit / Delete on the
|
|
1307
|
+
object-level check AND on the explain engine's record-grained verdict
|
|
1308
|
+
(`POST /api/v1/security/explain` with a `recordId`, ADR-0090 D6 / ADR-0095 C2 —
|
|
1309
|
+
the same pipeline the enforcement middleware runs, so button and server cannot
|
|
1310
|
+
disagree). Explaining oneself needs no special permission. The probe is one
|
|
1311
|
+
cached request per record, skipped entirely when the object-level check already
|
|
1312
|
+
says no, and **fails open** on every uncertainty — an unanswered hint must never
|
|
1313
|
+
be the reason a permitted user cannot act; the server stays the authority
|
|
1314
|
+
(ADR-0057 D10).
|
|
1315
|
+
|
|
1316
|
+
**A long option rendered straight past the combobox border.** `Combobox`'s
|
|
1317
|
+
trigger pinned itself to the component's `w-[200px]` default while the fields
|
|
1318
|
+
around it ran the full form column, and the selected label was a bare text child
|
|
1319
|
+
of a flex button — flex items need `truncate` AND `min-w-0` to clip, and it had
|
|
1320
|
+
neither. So "成员 (showcase_project_membership)" in the object picker overflowed
|
|
1321
|
+
the control and collided with the field beside it. The label now truncates, the
|
|
1322
|
+
trigger can shrink, the dropdown matches the trigger's width instead of a
|
|
1323
|
+
hardcoded 200px (a widened combobox used to clip its own options), and the two
|
|
1324
|
+
sharing-rule pickers ask for `w-full` so they line up with every other input.
|
|
1325
|
+
|
|
1326
|
+
Hardens `evaluatePermission` while there: a role config carrying only
|
|
1327
|
+
`fieldPermissions` (no `actions`) made `check()` throw a TypeError that
|
|
1328
|
+
propagated out of the render. A permission check must not be able to crash a
|
|
1329
|
+
view.
|
|
1330
|
+
|
|
1331
|
+
Browser-verified against the framework showcase Console in Chinese: object /
|
|
1332
|
+
criteria / recipient copy is fully localized, the recipient dropdown lists real
|
|
1333
|
+
users, business units and positions, a saved rule reopens with its recipient and
|
|
1334
|
+
criteria intact, editing the filter no longer submits, and a rule created
|
|
1335
|
+
end-to-end stores a real record id rather than free text. The criteria authored
|
|
1336
|
+
in the builder is honored by the evaluator: `{"pinned":true}` on an owner-private
|
|
1337
|
+
object granted the recipient exactly the matching records and nothing else.
|
|
1338
|
+
|
|
1339
|
+
- Updated dependencies [1767124]
|
|
1340
|
+
- Updated dependencies [8ecf5a6]
|
|
1341
|
+
- Updated dependencies [dfd3705]
|
|
1342
|
+
- Updated dependencies [6dee2cb]
|
|
1343
|
+
- Updated dependencies [c7cff19]
|
|
1344
|
+
- Updated dependencies [cd09a7b]
|
|
1345
|
+
- Updated dependencies [f1abf0e]
|
|
1346
|
+
- Updated dependencies [f05b84e]
|
|
1347
|
+
- Updated dependencies [662bdf9]
|
|
1348
|
+
- Updated dependencies [059a052]
|
|
1349
|
+
- Updated dependencies [53642d4]
|
|
1350
|
+
- Updated dependencies [8aae006]
|
|
1351
|
+
- Updated dependencies [d147a13]
|
|
1352
|
+
- @object-ui/types@17.0.0
|
|
1353
|
+
|
|
1354
|
+
## 16.1.0
|
|
1355
|
+
|
|
1356
|
+
### Minor Changes
|
|
1357
|
+
|
|
1358
|
+
- 1c8935a: feat(app-shell): render ActionParamDialog params through the shared form field-widget renderer (ADR-0059, #2700)
|
|
1359
|
+
|
|
1360
|
+
`ActionParamDialog` no longer hand-rolls a per-type ternary chain (select /
|
|
1361
|
+
lookup / textarea / number / boolean, everything else → text input). Every
|
|
1362
|
+
declared action param now renders through the same `fieldWidgetMap` the object
|
|
1363
|
+
form uses, so a param of ANY form-supported field type — `file`, `image`,
|
|
1364
|
+
`richtext`, `markdown`, `color`, `address`, `code`, `date`, … — gets its real
|
|
1365
|
+
widget, lazily loaded behind `Suspense`. Subsumes the single `file` branch ask
|
|
1366
|
+
in #2698: `type: 'file'` params render the real `FileField` upload control via
|
|
1367
|
+
the ambient `UploadProvider`, honoring `multiple`/`accept`/`maxSize`.
|
|
1368
|
+
|
|
1369
|
+
- `@object-ui/fields`: new exports `resolveFormWidgetType(type)` (widget-key
|
|
1370
|
+
resolution incl. spec aliases, text fallback) and `getLazyFieldWidget(type)`
|
|
1371
|
+
(per-type-cached `React.lazy` over the form's own widget loaders).
|
|
1372
|
+
- `@object-ui/core`: `ActionParamDef` gains `accept`/`maxSize`; `multiple` is
|
|
1373
|
+
now general widget config (was lookup-only).
|
|
1374
|
+
- `@object-ui/app-shell`: new pure `paramToField()` adapter (param → field
|
|
1375
|
+
shape) with a drift test pinning param support ⊇ form support (`FORM_FIELD_TYPES`),
|
|
1376
|
+
mirroring the FieldEditWidget parity guard; `resolveActionParams()` inherits
|
|
1377
|
+
`multiple`/`accept`/`maxSize` from the referenced field for every type.
|
|
1378
|
+
`required` validation, `visible` CEL gating, helpText, error styling, and
|
|
1379
|
+
value shapes for previously-supported types are unchanged.
|
|
1380
|
+
|
|
1381
|
+
- 2e7d7f0: feat(evaluator): route `{ dialect: 'cel' }` component/action predicates to the canonical CEL engine (#2661)
|
|
1382
|
+
|
|
1383
|
+
Component and action `visible` / `disabled` / `hidden` predicates were evaluated
|
|
1384
|
+
by the home-grown JS `ExpressionEvaluator`, while field rules
|
|
1385
|
+
(`visibleWhen`/`readonlyWhen`/`requiredWhen`, via `fieldRules.ts`) and row/list
|
|
1386
|
+
conditionals (via `evalRowPredicate`) already delegate to the canonical
|
|
1387
|
+
`@objectstack/formula` engine. That split meant a `{ dialect: 'cel' }` predicate
|
|
1388
|
+
in a renderer/action surface was executed as **JavaScript** — CEL-only forms
|
|
1389
|
+
(`x in list`, `has()`, typed `==`, the `today()`/`daysFromNow()` catalog) behaved
|
|
1390
|
+
differently from, or faulted against, the server's enforcement.
|
|
1391
|
+
|
|
1392
|
+
This converges the remaining tier onto the same engine:
|
|
1393
|
+
|
|
1394
|
+
- **`@object-ui/core`** — `ExpressionEvaluator.evaluateCondition` now detects a
|
|
1395
|
+
`{ dialect: 'cel', source }` envelope and evaluates it on `@objectstack/formula`
|
|
1396
|
+
(via `evalFieldPredicate`), binding the `record` namespace plus the whole
|
|
1397
|
+
context bag as top-level scope (`record.*`, `features.*`, `user.*`, `app.*`).
|
|
1398
|
+
Fail-soft to visible/enabled to match the legacy default; `throwOnError`
|
|
1399
|
+
callers still fail closed on a _faulting_ predicate (a genuine `false` never
|
|
1400
|
+
throws). This fixes every `SchemaRenderer` visibility/disabled read at once.
|
|
1401
|
+
- **`@object-ui/react`** — `toPredicateInput` preserves a CEL envelope instead of
|
|
1402
|
+
collapsing it to a `${source}` string, and `useCondition` accepts and forwards
|
|
1403
|
+
the envelope (keyed on a stable `(dialect, source)` so it doesn't re-evaluate
|
|
1404
|
+
each render). Action buttons (`action-icon`/`group`/`bar`/`button`) therefore
|
|
1405
|
+
evaluate CEL `visible`/`enabled`/`disabled` on the canonical engine.
|
|
1406
|
+
|
|
1407
|
+
**Back-compat:** bare strings and `${…}` templates stay on the legacy JS path
|
|
1408
|
+
(deprecation window); only an explicit `{ dialect: 'cel' }` envelope is rerouted.
|
|
1409
|
+
`{ dialect: 'template' }` is unaffected.
|
|
1410
|
+
|
|
1411
|
+
Together with the `^15.1.1` alignment (#2662), a renderer CEL predicate now
|
|
1412
|
+
reaches the identical verdict as the server — including the framework's
|
|
1413
|
+
`dateField == today()` equality fix (objectstack-ai/objectstack#3205) once it
|
|
1414
|
+
lands in a published 15.x. The broader home-grown-vs-canonical divergence
|
|
1415
|
+
motivation is #2661.
|
|
1416
|
+
|
|
1417
|
+
- 31b77d4: **Add the explicit `engine-owned` lifecycle bucket (tracks framework ADR-0103 addendum / #3343).** The framework split the overloaded `managedBy: 'system'` bucket by promoting the engine-owned case to its own enum value; this mirrors it in the UI type + runtime + badge.
|
|
1418
|
+
|
|
1419
|
+
- **`@object-ui/types`** — `ManagedByBucket` union and `MANAGED_BY_BUCKETS` gain `'engine-owned'` (canonical order: `platform, config, system, engine-owned, append-only, better-auth`). The union stays closed, so every consumer that missed the new value is a compile error.
|
|
1420
|
+
- **`@object-ui/core`** — `resolveCrudAffordances` gains the `engine-owned` default row (identical all-locked matrix as `system`/`append-only`), so `isObjectInlineEditable` / the grid + form gates treat it as read-only automatically.
|
|
1421
|
+
- **`@object-ui/app-shell`** — the `ManagedByBadge` renders `engine-owned` with the same read-only "System-managed" copy as a locked `system` object (reuses the existing `managedByBadge.system` i18n key — zero translation churn; the distinction is at the schema level, not the user-facing string), and `resolveManagedByEmptyState` reuses the `system` engine-owned empty state.
|
|
1422
|
+
|
|
1423
|
+
Behaviour-preserving: `engine-owned` resolves to the same locked affordances `system` did by default, so nothing about how a locked object renders changes — the value just makes the schema self-documenting. New unit coverage for the bucket in `resolveCrudAffordances` / `isObjectInlineEditable` / `MANAGED_BY_BUCKETS` / the empty-state helper.
|
|
1424
|
+
|
|
1425
|
+
- 62b9ab5: feat(data): unify master-detail saves behind `DataSource.batchTransaction`, isolate the non-atomic fallback in the adapter (#2679)
|
|
1426
|
+
|
|
1427
|
+
Master-detail saves (`MasterDetailForm`, `LineItemsPanel`) now always persist
|
|
1428
|
+
through `dataSource.batchTransaction(operations)` — one ordered cross-object
|
|
1429
|
+
operation list, with `{ $ref: <op index> }` linking a child to a parent created
|
|
1430
|
+
in the same batch. The form no longer contains any client-side orchestration or
|
|
1431
|
+
best-effort compensation-delete; that atomicity anti-pattern is gone from the UI
|
|
1432
|
+
layer (framework #1604 / framework ADR-0034 item 4).
|
|
1433
|
+
|
|
1434
|
+
- **`@object-ui/types`** — `batchTransaction?` is now a first-class (optional)
|
|
1435
|
+
method on the `DataSource` contract, typed via `BatchTransactionOperation` /
|
|
1436
|
+
`BatchRef`. Replaces the previous `(dataSource as any).batchTransaction`
|
|
1437
|
+
method-sniffing.
|
|
1438
|
+
- **`@object-ui/core`** — new `emulateBatchTransaction(dataSource, operations)`
|
|
1439
|
+
(sequential writes, `$ref` resolution, best-effort reverse-order compensation)
|
|
1440
|
+
and `runBatchTransaction(dataSource, operations)` (prefers the adapter's method,
|
|
1441
|
+
emulates otherwise). `ApiDataSource` / `ValueDataSource` implement
|
|
1442
|
+
`batchTransaction` via the emulation.
|
|
1443
|
+
- **`@object-ui/data-objectstack`** — `ObjectStackAdapter.batchTransaction` uses
|
|
1444
|
+
the server's atomic `POST /api/v1/batch`, prefers the typed
|
|
1445
|
+
`client.data.batchTransaction` SDK method when the installed client exposes it,
|
|
1446
|
+
and degrades to the client-side emulation ONLY when the endpoint is missing
|
|
1447
|
+
(404/405) or the runtime can't do transactions (501). Real errors (400/401/403/
|
|
1448
|
+
409/500) still surface. This is the isolated, tested home of the non-atomic
|
|
1449
|
+
fallback.
|
|
1450
|
+
- **`@object-ui/plugin-form`** — removed `applyDetail` / `createMany` /
|
|
1451
|
+
`ApplyDetailResult` from `masterDetailTx.ts`; `MasterDetailForm` and
|
|
1452
|
+
`LineItemsPanel` build ops and call `runBatchTransaction`. `LineItemsPanel`
|
|
1453
|
+
saves are now atomic on a capable backend, with the rollup folded into the same
|
|
1454
|
+
batch.
|
|
1455
|
+
|
|
1456
|
+
No behavior change on a current ObjectStack backend (it has `/api/v1/batch`);
|
|
1457
|
+
older/limited backends keep a working — now clearly non-atomic — save path.
|
|
1458
|
+
|
|
1459
|
+
- 1629313: feat(fields): RadioField per-option `visibleWhen` cascading + `dependsOn` gating; single-source the option resolver
|
|
1460
|
+
|
|
1461
|
+
Brings `RadioField` to parity with `SelectField` / `MultiSelectField` for ADR-0058
|
|
1462
|
+
cascading & role-gated options, and collapses the three copies of the
|
|
1463
|
+
gate-then-filter logic onto one shared resolver.
|
|
1464
|
+
|
|
1465
|
+
- **`@object-ui/core`**: new pure `resolveCascadingOptions(rawOptions, record, dependsOn, scope)`
|
|
1466
|
+
→ `{ options, gated, dependsOnFields }` — the single source of truth for
|
|
1467
|
+
`dependsOn` gating + per-option `visibleWhen` filtering.
|
|
1468
|
+
- **`@object-ui/fields`**: `RadioField` now narrows its offered radios against
|
|
1469
|
+
the live record + `current_user`, gates behind a "select the parent first"
|
|
1470
|
+
hint while a `dependsOn` field is empty, and clears a value no longer offered
|
|
1471
|
+
(scalar cascade clear). The `useCascadingOptions` hook is refactored to a thin
|
|
1472
|
+
React wrapper over `resolveCascadingOptions`.
|
|
1473
|
+
- **`@object-ui/components`**: the form renderer's inline option pre-filter and
|
|
1474
|
+
cross-field cascade-clear effect now call `resolveCascadingOptions` instead of
|
|
1475
|
+
re-deriving gating/filtering, so they can't drift from the widgets (no
|
|
1476
|
+
behavior change).
|
|
1477
|
+
- Tests: `RadioField.cascade.test.tsx` mirrors the select cascade tests; core
|
|
1478
|
+
gains `resolveCascadingOptions` unit coverage.
|
|
1479
|
+
|
|
1480
|
+
- 2331ac9: feat(report): drill a date-bucket cell into its time range, not a superset (#1752)
|
|
1481
|
+
|
|
1482
|
+
Clicking a report/dashboard cell grouped by a `dateGranularity` date dimension
|
|
1483
|
+
("2026-Q2") used to drill into a **superset** — the date dimension was skipped,
|
|
1484
|
+
so the record list spanned every time bucket. It now scopes to the clicked
|
|
1485
|
+
bucket's half-open range, consuming the framework's new `drillRanges` sidecar.
|
|
1486
|
+
|
|
1487
|
+
- **`@object-ui/core`** — `buildDatasetDrillFilter` accepts the per-row
|
|
1488
|
+
`drillRanges` and emits an ObjectQL range operator object
|
|
1489
|
+
(`{ [field]: { $gte, $lt } }`) alongside the equality dims.
|
|
1490
|
+
- **`@object-ui/plugin-report` / `@object-ui/plugin-dashboard`** — the report
|
|
1491
|
+
renderer and dashboard widget forward `drillRanges`, and a **date-only**
|
|
1492
|
+
report (no equality drill dim) is now drillable via the range alone.
|
|
1493
|
+
- **`@object-ui/app-shell`** — the "Open in list →" escape hatch
|
|
1494
|
+
(`useOpenRecordList`) now targets the ADR-0055 **bare data surface**
|
|
1495
|
+
(`/:object/data`, "the URL is the view" — no baked-in view filter to
|
|
1496
|
+
over-narrow the drill) and serializes a range to the
|
|
1497
|
+
`filter[field][gte|lt]` operator contract. `ObjectDataPage` parses those
|
|
1498
|
+
operators (equality shorthand unchanged), renders a range as a single chip,
|
|
1499
|
+
and removes both bounds together. A new `drillUrlFilters` module owns the
|
|
1500
|
+
write/read serialization so both sides can't drift (round-trip tested).
|
|
1501
|
+
|
|
1502
|
+
Companion to the framework analytics change (objectstack-ai/objectstack#3256).
|
|
1503
|
+
|
|
1504
|
+
### Patch Changes
|
|
1505
|
+
|
|
1506
|
+
- 8b8b744: chore(deps): align `@objectstack/formula` / `lint` / `client` to `^15.1.1`
|
|
1507
|
+
|
|
1508
|
+
These three were still pinned to `^14.6.0` while `@objectstack/spec` was already
|
|
1509
|
+
`^15.1.1` — a version skew from the v15 upgrade (formula/lint/client publish in
|
|
1510
|
+
lockstep with spec, and their own 15.0.0 entries are pure dependency bumps, so
|
|
1511
|
+
this is alignment, not a behavioral migration).
|
|
1512
|
+
|
|
1513
|
+
Practical effect: the client-side field-rule evaluation
|
|
1514
|
+
(`visibleWhen`/`readonlyWhen`/`requiredWhen` via `fieldRules.ts`, which delegates
|
|
1515
|
+
to `@objectstack/formula`'s `ExpressionEngine`) now tracks the 15.x engine — and
|
|
1516
|
+
will pick up the framework's `dateField == today()` equality fix
|
|
1517
|
+
(objectstack-ai/objectstack#3205) automatically at the next 15.x release via the
|
|
1518
|
+
caret range. Renderer/action `visible`/`disabled` predicates are unaffected (they
|
|
1519
|
+
use the home-grown JS evaluator — tracked separately in #2661).
|
|
1520
|
+
|
|
1521
|
+
- 7cf4051: chore(deps): align every `@objectstack/*` dependency to `^16.0.0-rc.0`
|
|
1522
|
+
|
|
1523
|
+
Bumps `@objectstack/spec` / `client` / `formula` / `lint` from `^15.1.1` to the
|
|
1524
|
+
`16.0.0-rc.0` pre-release across the workspace (root + `apps/console` +
|
|
1525
|
+
`apps/site` + all consuming packages). ObjectUI's own packages are already on
|
|
1526
|
+
major 16, so this closes the 15↔16 skew between ObjectUI and the `@objectstack`
|
|
1527
|
+
contract libraries (which publish in lockstep with `spec`).
|
|
1528
|
+
|
|
1529
|
+
This is a dependency alignment, not a behavioral migration: the full workspace
|
|
1530
|
+
build (43/43) and the `@objectstack`-consuming package test suites
|
|
1531
|
+
(`core` / `app-shell` / `data-objectstack` / `plugin-form` / `types`) are green
|
|
1532
|
+
against `16.0.0-rc.0` with no source changes required.
|
|
1533
|
+
|
|
1534
|
+
Practical effect: `@objectstack/client@16.0.0-rc.0` now ships
|
|
1535
|
+
`data.batchTransaction` (framework #3271), so `ObjectStackAdapter`'s feature
|
|
1536
|
+
detect (`typeof client.data.batchTransaction === 'function'`) routes
|
|
1537
|
+
master-detail cross-object saves through the typed SDK method instead of the
|
|
1538
|
+
raw `fetch('/api/v1/batch')` fallback — realizing the "verify SDK path" half of
|
|
1539
|
+
#2694. The raw-fetch branch stays as a defensive fallback (removal tracked in
|
|
1540
|
+
#2694).
|
|
1541
|
+
|
|
1542
|
+
- 6d4fbe6: **Consolidate the `managedBy` lifecycle-bucket logic into one shared source of truth (follows framework ADR-0103).** The bucket taxonomy was hand-mirrored in several places — `crudAffordances.ts`, `ManagedByBadge.tsx` (its own `Bucket` union + `isWriteOptedIn` + the writable-system derivation), and `plugin-detail`'s `record-details.tsx` (`NON_EDITABLE_BUCKETS`, duplicated because it can't depend on app-shell) — a drift risk, and the object-schema `managedBy` type was open-ended (`(string & {})`) so unknown buckets slipped through and silently defaulted to fully-editable.
|
|
1543
|
+
|
|
1544
|
+
- **`@object-ui/types`** now owns the closed `ManagedByBucket` union (+ `MANAGED_BY_BUCKETS`), and `ObjectSchema.managedBy` is tightened from `'platform' | 'better-auth' | (string & {})` to that union — unknown buckets are now a type error at authoring time.
|
|
1545
|
+
- **`@object-ui/core`** now owns the React-free runtime logic — `resolveCrudAffordances`, `isWriteOptedIn`, `isSystemWritable`, `isObjectInlineEditable` — reachable by every UI package including `plugin-detail` (which could not import app-shell).
|
|
1546
|
+
- **`app-shell/utils/crudAffordances.ts`** is now a thin re-export of `@object-ui/core` (existing imports keep working); `ManagedByBadge` consumes the shared `isSystemWritable`; `plugin-detail` `record-details.tsx` replaces its hand-mirrored `NON_EDITABLE_BUCKETS` with `isObjectInlineEditable`.
|
|
1547
|
+
|
|
1548
|
+
Behavior-preserving — all existing affordance/edit-gate tests stay green; the shared module adds direct unit coverage (including the previously-untested `isSystemWritable` derivation). Translated copy (badge variants, empty-state messages) stays in app-shell.
|
|
1549
|
+
|
|
1550
|
+
- 0a3710b: **Finish the `managedBy` / `userActions` de-dup — one parser for the override shape (completes objectui#2712, framework#3343).** #2712 consolidated the bucket _union_ + affordance _set_ mirrors but left four surfaces still parsing the `userActions.{create,edit,delete}` override shape by hand. They now all route through the shared `@object-ui/core` policy, so no package re-implements the boolean / #2614-object-form parse locally.
|
|
1551
|
+
|
|
1552
|
+
- **`@object-ui/core`** promotes the internal `normalizeOverride` to the exported **`normalizeUserAction(v, base)`** (the one parser) and adds **`userActionPredicates(v)`** for per-record CEL predicate extraction.
|
|
1553
|
+
- **`app-shell/utils/managedByEmptyState.ts`** — the writable-`system` create check and its local `EmptyStateUserActions` interface are replaced by `resolveCrudAffordances({ managedBy, userActions }).create`.
|
|
1554
|
+
- **`plugin-grid/rowCrudAffordances.ts`** — the local `isOptedOut` / `predicatesOf` helpers (and duplicated `RowCrudUserAction` / `RowCrudPredicates` types) fold into `normalizeUserAction`; the historical type names stay re-exported for compat.
|
|
1555
|
+
- **`plugin-detail/RelatedList.tsx`** — its inline `predicatesOf` fold into `userActionPredicates`.
|
|
1556
|
+
- **`plugin-form/ObjectForm.tsx`** — the hand-rolled `managedBy !== 'platform'` blanket lock + `userActions` unlock is replaced by the resolved affordance for the current mode (`edit` / `create`), the **same** `resolveCrudAffordances` contract the detail (`isObjectInlineEditable`) and grid surfaces use.
|
|
1557
|
+
|
|
1558
|
+
Behavior-preserving for `platform` / `system` / `append-only` / `better-auth`, with one deliberate alignment: an admin-editable **`config`**-bucket object (e.g. `sys_webhook`, `sys_permission_set`) is now editable in `ObjectForm` — it was previously over-locked as "non-`platform`", while detail/grid already treated it as editable (`config` resolves `edit: true`). New unit coverage for the shared parser and the config / create-mode form gate; all existing affordance/edit-gate tests stay green.
|
|
1559
|
+
|
|
1560
|
+
- eee4ded: feat(fields): render `select` + `multiple` through the multi-value chip picker; restore fields/core lint gates
|
|
1561
|
+
|
|
1562
|
+
- **Multi-value select** — a `select` field/param declared `multiple: true`
|
|
1563
|
+
now renders the multi-value chip picker (the `multiselect` widget) and stores
|
|
1564
|
+
a `string[]`, instead of collapsing to a single-value dropdown that could
|
|
1565
|
+
hold only one value. The delegation lives inside `SelectField`, so the object
|
|
1566
|
+
form, the inline grid editor, and the app-shell `ActionParamDialog` all
|
|
1567
|
+
inherit it from the one `select` widget with no per-surface drift. Single
|
|
1568
|
+
selects keep the cascading dropdown (multi + per-option `visibleWhen`
|
|
1569
|
+
cascading is not a combination in use today).
|
|
1570
|
+
- **`autonumber` mapping is unchanged** here; this change is orthogonal.
|
|
1571
|
+
- **Lint gates restored** — fixed the pre-existing baseline lint errors that
|
|
1572
|
+
had left the `@object-ui/fields` and `@object-ui/core` package lints red (so
|
|
1573
|
+
the gate could not catch new violations): `react-hooks/rules-of-hooks` in
|
|
1574
|
+
`ImageField` / `TextAreaField` / `index.tsx` (hooks hoisted above early
|
|
1575
|
+
returns; the `useFieldTranslate` hook no longer wrapped in try/catch), plus
|
|
1576
|
+
`no-useless-assignment` / `no-useless-escape` / `no-control-regex` /
|
|
1577
|
+
`prefer-const` / `preserve-caught-error` in the core evaluator and utils. No
|
|
1578
|
+
behavior change from the lint fixes.
|
|
1579
|
+
|
|
1580
|
+
- Updated dependencies [7cf4051]
|
|
1581
|
+
- Updated dependencies [94d4876]
|
|
1582
|
+
- Updated dependencies [2b17339]
|
|
1583
|
+
- Updated dependencies [31b77d4]
|
|
1584
|
+
- Updated dependencies [6d4fbe6]
|
|
1585
|
+
- Updated dependencies [62b9ab5]
|
|
1586
|
+
- Updated dependencies [29c6040]
|
|
1587
|
+
- Updated dependencies [faebac3]
|
|
1588
|
+
- Updated dependencies [199fa83]
|
|
1589
|
+
- @object-ui/types@16.1.0
|
|
1590
|
+
|
|
3
1591
|
## 16.0.0
|
|
4
1592
|
|
|
5
1593
|
### Patch Changes
|
|
@@ -499,7 +2087,7 @@
|
|
|
499
2087
|
(plugin-charts) and `DatasetWidget` (plugin-dashboard) both use it, so
|
|
500
2088
|
multi-dimension charts render consistently as grouped/coloured bars.
|
|
501
2089
|
|
|
502
|
-
Refs objectstack-ai/objectui#1759, objectstack-ai/
|
|
2090
|
+
Refs objectstack-ai/objectui#1759, objectstack-ai/objectstack#1890
|
|
503
2091
|
|
|
504
2092
|
- 7c239fd: Add `ComponentRegistry.unregister(type, namespace?)` — the inverse of
|
|
505
2093
|
`register()`. Clears the namespaced key and the bare-name fallback (when it
|