@object-ui/core 17.5.0 → 17.6.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 +794 -0
- package/README.md +36 -22
- package/dist/actions/ActionEngine.d.ts +1 -1
- package/dist/actions/ActionEngine.js +1 -2
- package/dist/actions/ActionRunner.d.ts +82 -10
- package/dist/actions/ActionRunner.js +128 -22
- package/dist/actions/actionKeys.d.ts +68 -8
- package/dist/actions/actionKeys.js +81 -9
- package/dist/actions/actionResponse.js +1 -1
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/recordIdParam.d.ts +77 -0
- package/dist/actions/recordIdParam.js +37 -0
- package/dist/actions/serverActionHandler.d.ts +1 -1
- package/dist/actions/serverActionHandler.js +1 -1
- package/dist/evaluator/ExpressionEvaluator.d.ts +42 -1
- package/dist/evaluator/ExpressionEvaluator.js +42 -1
- package/dist/evaluator/SafeExpressionParser.js +0 -4
- package/dist/evaluator/declaredPredicate.js +1 -1
- package/dist/evaluator/fieldRules.d.ts +19 -0
- package/dist/evaluator/fieldRules.js +30 -6
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +49 -2
- package/dist/evaluator/listConditional.js +70 -6
- package/dist/evaluator/optionRules.d.ts +55 -0
- package/dist/evaluator/optionRules.js +60 -0
- package/dist/evaluator/rowPredicateCanon.d.ts +67 -0
- package/dist/evaluator/rowPredicateCanon.js +208 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +17 -0
- package/dist/registry/Registry.d.ts +41 -19
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/types/index.d.ts +92 -13
- package/dist/utils/chart-presentation.d.ts +205 -0
- package/dist/utils/chart-presentation.js +280 -0
- package/dist/utils/chart-series.d.ts +140 -26
- package/dist/utils/chart-series.js +380 -39
- package/dist/utils/column-identity.d.ts +43 -0
- package/dist/utils/column-identity.js +47 -1
- package/dist/utils/dataset-format.js +18 -6
- package/dist/utils/debug.js +7 -0
- package/dist/utils/expand-fields.d.ts +97 -7
- package/dist/utils/expand-fields.js +97 -7
- package/dist/utils/filter-tokens.js +0 -1
- package/dist/utils/freeze-schema.d.ts +38 -1
- package/dist/utils/freeze-schema.js +10 -0
- package/dist/utils/humanize-label.d.ts +60 -0
- package/dist/utils/humanize-label.js +62 -0
- package/dist/utils/managedBy.d.ts +27 -2
- package/dist/utils/normalize-list-view.d.ts +10 -0
- package/dist/utils/normalize-list-view.js +63 -1
- package/dist/utils/number-display.d.ts +7 -3
- package/dist/utils/predicate-fields.d.ts +29 -4
- package/dist/utils/predicate-fields.js +44 -4
- package/dist/utils/retired-field-types.d.ts +90 -0
- package/dist/utils/retired-field-types.js +124 -0
- package/dist/utils/sort-query.d.ts +12 -10
- package/dist/utils/unmaterialized-fields.d.ts +52 -0
- package/dist/utils/unmaterialized-fields.js +57 -0
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/schema-validator.js +105 -0
- package/dist/validation/server-owned-value.d.ts +62 -0
- package/dist/validation/server-owned-value.js +99 -0
- package/dist/validation/validators/object-validation-engine.js +0 -2
- package/package.json +4 -4
- package/dist/__benchmarks__/core.bench.d.ts +0 -8
- package/dist/__benchmarks__/core.bench.js +0 -53
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,799 @@
|
|
|
1
1
|
# @object-ui/core
|
|
2
2
|
|
|
3
|
+
## 17.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 279fb13: `ComponentInput.type` can declare a UNION, so a block stops warning about legal
|
|
8
|
+
writes its own description recommends
|
|
9
|
+
|
|
10
|
+
A registration's `type` was one coarse control kind, while a good number of spec
|
|
11
|
+
keys accept more than one shape. A declaration therefore had to pick an arm, and
|
|
12
|
+
the repo's own manifest gate then reported `type-mismatch` on the other arm's
|
|
13
|
+
legal values. Four of the five measured cases were the loud shape: the input's
|
|
14
|
+
`description` teaches the author to write an inline translation map
|
|
15
|
+
(`{ en, "zh-CN", … }`) while the same input's `type: 'string'` made
|
|
16
|
+
`sdui-parser`'s `checkType` warn about exactly that map — one platform authority
|
|
17
|
+
contradicting itself on the write it had just recommended. Because these land at
|
|
18
|
+
warning severity the page still compiled and rendered; the cost is that noise on
|
|
19
|
+
correct authoring trains authors, AI authors included, to dismiss the
|
|
20
|
+
`unknown-prop` and `type-mismatch` reports that are real.
|
|
21
|
+
|
|
22
|
+
`type` now accepts an ARRAY of coarse kinds as well as a single one (maintainer
|
|
23
|
+
ruling on objectui#3832, direction (a)), and a value passes the coarse check when
|
|
24
|
+
ANY declared arm accepts it. Both declaration sites in `@object-ui/types` move
|
|
25
|
+
together with the registry's own copy in `@object-ui/core`, and
|
|
26
|
+
`ComponentInputSchema` enforces the same widening — a non-empty array of
|
|
27
|
+
DISTINCT kinds, so an empty arm list or a repeated arm is refused at authoring
|
|
28
|
+
time rather than normalized behind the author's back.
|
|
29
|
+
|
|
30
|
+
Five declarations now spell their real contract, and the `type-mismatch` warning
|
|
31
|
+
on each of these legal writes is gone:
|
|
32
|
+
|
|
33
|
+
- `page:header.title`, `page:header.subtitle`, `page:card.title` —
|
|
34
|
+
string **or** inline translation map (the spec's union, measured against
|
|
35
|
+
`ComponentPropsMap` at the pinned rc.6; the renderers resolve both through
|
|
36
|
+
`pickLocalized`);
|
|
37
|
+
- `record:alert.title`, `record:alert.body` — the same two shapes, justified
|
|
38
|
+
against the RENDERER since the pinned spec carries no `record:alert` props
|
|
39
|
+
schema;
|
|
40
|
+
- `element:text_input.defaultValue` — `string | number`, the spec's union,
|
|
41
|
+
which had been narrowed to `'string'` with the number arm named only in prose.
|
|
42
|
+
|
|
43
|
+
**Backward compatible, and measured as such.** The single-kind form stays valid
|
|
44
|
+
and is still the canonical spelling for a one-arm key: it validates identically
|
|
45
|
+
(the diagnostics for one arm, `invalid-enum` and its `error` severity included,
|
|
46
|
+
are byte-identical), and `manifestFromConfigs` collapses a one-element array back
|
|
47
|
+
to the bare string, so every entry already in a published `sdui.manifest.json`
|
|
48
|
+
serializes unchanged and arrays appear only where a union was really declared.
|
|
49
|
+
The JSX authoring surface follows in the same step — `generateDts` emits a
|
|
50
|
+
TypeScript union for a union input, so the `.d.ts` an author type-checks against
|
|
51
|
+
accepts exactly what the gate accepts.
|
|
52
|
+
|
|
53
|
+
A union widens what is legal; it does not switch the check off. A value matching
|
|
54
|
+
NO declared arm is still reported, a multi-arm mismatch reports at its strictest
|
|
55
|
+
arm's severity (`error` when an `enum` arm is present, so an enum's closed list
|
|
56
|
+
does not become dismissible by having a second arm added next to it), and arms
|
|
57
|
+
are meant to match the contract rather than relax the gate:
|
|
58
|
+
`element:text_input.defaultValue` deliberately gains no `object` arm because the
|
|
59
|
+
spec rejects a map there, and `element:record_picker.emptyText` keeps its single
|
|
60
|
+
`'string'` arm because that renderer drops the map form (objectui#4163) — an arm
|
|
61
|
+
the renderer never honours would advertise a shape that cannot reach the screen.
|
|
62
|
+
- e1d4251: Action param `visible`: one dialect answer, and a fault that is fail-open and LOUD
|
|
63
|
+
|
|
64
|
+
`ActionParamDialog`'s `filterVisibleParams` was the last predicate face never
|
|
65
|
+
converted to the canonical entry. It evaluated each param's `visible` on a bare
|
|
66
|
+
`ExpressionEvaluator` inside `try { … } catch { return true }`, and that produced
|
|
67
|
+
two defects at once (objectui#4640, measured on `main`):
|
|
68
|
+
|
|
69
|
+
- **Silence.** Three of the four fault shapes emitted nothing at all — an
|
|
70
|
+
unparseable source, an unbound identifier and a faulting legacy predicate all
|
|
71
|
+
resolved without a word, so a broken `visible` was indistinguishable from an
|
|
72
|
+
absent one. The standing 2026-08-06 ruling on objectui#4051 /
|
|
73
|
+
objectstack#5149 names silence as the one option that is not available.
|
|
74
|
+
- **The fail DIRECTION was decided by the predicate's dialect, not by the
|
|
75
|
+
surface.** A bare string ran the legacy JS evaluator (lenient → falsy → param
|
|
76
|
+
silently DROPPED); a `{ dialect, source }` envelope ran CEL (fault → param
|
|
77
|
+
silently KEPT). One `visible` key, two opposite outcomes, chosen by whether
|
|
78
|
+
the authored text happened to contain `${…}` / `===` — the objectui#3314
|
|
79
|
+
shape. Both halves hurt: a dropped param means the dialog never collects a
|
|
80
|
+
value the server requires and the action fails at submit with nothing pointing
|
|
81
|
+
at the predicate; a kept one offers a field the backend rejects.
|
|
82
|
+
|
|
83
|
+
`filterVisibleParams` now routes through `evalRowPredicate`. A param whose
|
|
84
|
+
`visible` cannot be evaluated is **shown**, and reported once, with the action
|
|
85
|
+
and the param named and the predicate quoted. Fail-open is the ruled direction
|
|
86
|
+
for this surface: an extra offered field is rejected by the server with a
|
|
87
|
+
message, while a silently hidden required param is undiagnosable. (Row surfaces
|
|
88
|
+
keep failing closed — there the harm runs the other way.) Boolean and blank
|
|
89
|
+
predicates are answered before the evaluator, so `visible: false` hides the
|
|
90
|
+
param and an empty predicate is not reported as broken.
|
|
91
|
+
|
|
92
|
+
**Behaviour change worth knowing before you upgrade.** On the canonical CEL
|
|
93
|
+
engine an ABSENT key is a runtime fault, not a falsy read. A param gated on
|
|
94
|
+
`features.phoneNumber == true` in a deployment whose scope carries no
|
|
95
|
+
`phoneNumber` key at all now takes the fail-open branch: the param is SHOWN,
|
|
96
|
+
with a warning naming it, where it used to be hidden. The conservative outcome
|
|
97
|
+
is still available, in the spelling that is portable to the server's own engine:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
has(features.phoneNumber) && features.phoneNumber == true
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Deployments that DECLARE the flag (`features: { phoneNumber: false }`) are
|
|
104
|
+
unaffected — that is a genuine verdict on both engines, and it did not move.
|
|
105
|
+
|
|
106
|
+
`@object-ui/core` gains the two evaluator changes this needed:
|
|
107
|
+
|
|
108
|
+
- `evalRowPredicate` accepts **`rowless`** — "this surface has no row of its
|
|
109
|
+
own", so nothing is bound over the host scope and a `record` / `data` the
|
|
110
|
+
scope carries survives instead of being shadowed by an empty row. Row surfaces
|
|
111
|
+
are untouched: without the option the row is still the subject (objectui#3796).
|
|
112
|
+
- A faulting **`{ dialect, source }` envelope now reports its own source**.
|
|
113
|
+
It used to print the literal `"(expression)"`, and because the warn-once key
|
|
114
|
+
is (label, predicate), the first faulting envelope under a label silenced
|
|
115
|
+
every other one. The envelope is what `@objectstack/spec` normalizes every
|
|
116
|
+
authored predicate into, so this was the likeliest shape in served metadata
|
|
117
|
+
and the least diagnosable one.
|
|
118
|
+
- 167ec42: `ComponentMeta.labelling` grows a third value: `'control' | 'group' | 'display'`
|
|
119
|
+
(objectui#4857, ruled jointly with objectui#4871 as the single repo-wide vocabulary for
|
|
120
|
+
"how does a host learn what a widget will render"). `'display'` declares a widget whose
|
|
121
|
+
whole surface is a pure display in EVERY state — no focusable control, nothing a
|
|
122
|
+
`<label for>` could ever reach.
|
|
123
|
+
|
|
124
|
+
The form renderer answers the declaration with the objectui#4788 host container (field
|
|
125
|
+
id + `aria-labelledby` + `aria-describedby` + `role="group"`) in the editable state too;
|
|
126
|
+
the `readonly === true` arm keeps its exact #4788 semantics for undeclared widgets. The
|
|
127
|
+
display-only four (`formula` / `summary` / `auto_number` / `vector`) declare `'display'`
|
|
128
|
+
— on the real object-form path they arrive `disabled`, never `readonly` (a deliberate
|
|
129
|
+
distinction this change does not touch), so their visible labels pointed `for` at an id
|
|
130
|
+
no element carried and their help text had zero consumers in every editable form.
|
|
131
|
+
|
|
132
|
+
`grid` was re-measured before being classified: its only bare-config focusable is the
|
|
133
|
+
auxiliary "Add line" button (routing `for` there would have label clicks insert rows),
|
|
134
|
+
and every realistic config is a table of per-cell inputs — a composite. It declares
|
|
135
|
+
`labelling: 'group'` and its root container now consumes the host id, name and
|
|
136
|
+
description, exactly like `address` / `checkboxes`.
|
|
137
|
+
|
|
138
|
+
Companion registry gate: `FIELD_WIDGET_LABELLING` (exported) is a `Record` keyed by the
|
|
139
|
+
field-widget map's own literal key union, so registering a widget without deciding its
|
|
140
|
+
labelling is a compile error rather than a silent fall-through to the dangling-`for`
|
|
141
|
+
path, and the declaration test asserts the registered meta agrees with it key by key.
|
|
142
|
+
- b1119ec: Project every declared `recordIdField`, and refuse an action that names no record
|
|
143
|
+
|
|
144
|
+
objectstack#8018. An `api` action declaring `recordIdParam` identifies the record
|
|
145
|
+
it acts on by a row field — `recordIdField`, default `id`. The grid built
|
|
146
|
+
`$select` from the listView columns, `id` and the predicate refs only, so an
|
|
147
|
+
action keyed on any other field asked the server for everything except the key
|
|
148
|
+
naming its own record. The row arrived without it, and the injection was skipped
|
|
149
|
+
silently: the request went out anyway, minus the parameter. A backend reading a
|
|
150
|
+
missing selector as "match nothing" then answers success for having changed
|
|
151
|
+
nothing, so a record-scoped mutation reports success and does nothing.
|
|
152
|
+
|
|
153
|
+
Two independent repairs, both in this change:
|
|
154
|
+
|
|
155
|
+
- **Projection.** `listViewPredicates` (`@object-ui/core`) now also harvests
|
|
156
|
+
`recordIdField` from `rowActionDefs`, `bulkActionDefs` and the object's
|
|
157
|
+
`actions`, spelled as a synthetic `record.<name>` so the one existing harvester
|
|
158
|
+
handles it. Both projection builders — `ObjectGrid` and `ListView` — read that
|
|
159
|
+
function, so both gain the key with no call-site change. The existing guards
|
|
160
|
+
still apply: a name the object does not declare, or one that is not a bare
|
|
161
|
+
identifier, is dropped rather than put in `$select`, because an unknown key
|
|
162
|
+
there is not ignored by every backend.
|
|
163
|
+
- **Loud failure.** New `resolveRecordIdParamSeed` (`@object-ui/core`) is the one
|
|
164
|
+
definition of "can this row identify the record?". `useConsoleActionRuntime`'s
|
|
165
|
+
api handler now refuses the dispatch — `{ success: false, error }`, before the
|
|
166
|
+
request — when the row lacks the key, or holds `null` for it. The two refusals
|
|
167
|
+
are worded differently because they point at different repairs: an absent key
|
|
168
|
+
is a projection or read-visibility problem, a null value is a data one. Falsy
|
|
169
|
+
real values (`0`, `''`, `false`) are values and still dispatch.
|
|
170
|
+
|
|
171
|
+
The second half is what closes the class rather than the common case: a row can
|
|
172
|
+
lack the key for reasons projection cannot fix — a server-side read mask that
|
|
173
|
+
strips the field regardless of `$select`, a partial payload, a field the
|
|
174
|
+
principal cannot read.
|
|
175
|
+
|
|
176
|
+
Behaviour change worth noting: an action that previously dispatched an
|
|
177
|
+
under-specified request now fails visibly instead. That is the point — the old
|
|
178
|
+
path could not report the failure it was causing.
|
|
179
|
+
- af025ee: Draw a null second-dimension group instead of carrying its measure invisibly
|
|
180
|
+
|
|
181
|
+
objectui#4673. `buildChartSeries`' pivot branch kept the pre-objectui#4466
|
|
182
|
+
answer on the SECOND dimension: it bucketed groups by `String(row[groupKey] ??
|
|
183
|
+
'')` behind a `gId !== ''` gate, so a group whose second dimension is `null`,
|
|
184
|
+
`undefined` or `''` never joined the series list — while the line below the
|
|
185
|
+
gate still wrote its measure into the emitted row under the `''` key. The
|
|
186
|
+
number was in the data and bound to no mark, which is #4466's harm verbatim one
|
|
187
|
+
dimension over: the chart understated its own data without saying so.
|
|
188
|
+
|
|
189
|
+
Measured on the card's repro — `GROUP BY status, priority` over a Backlog with
|
|
190
|
+
5 hours at High priority and 40 hours at no priority — the transform emitted
|
|
191
|
+
`{status: 'Backlog', High: 5, '': 40}` with a single `High` series, and the
|
|
192
|
+
renderer drew exactly ONE bar. The 40 hours were present in the row, scaled for
|
|
193
|
+
on the y-axis, and painted on nothing.
|
|
194
|
+
|
|
195
|
+
Two such groups were worse than unbound: `null` and `''` both key `''`, so the
|
|
196
|
+
later group silently overwrote the earlier one's measure and one of the two
|
|
197
|
+
numbers did not survive the transform at all.
|
|
198
|
+
|
|
199
|
+
**A known-empty group now draws; an unprojected key still refuses.** That split
|
|
200
|
+
is the doctrine the first dimension already used (objectui#4466 versus
|
|
201
|
+
`hasNoCategoryKey`, framework#4033), and it now answers the same way on both
|
|
202
|
+
dimensions. Concretely, a row that does not carry the group key at all gets no
|
|
203
|
+
bucket and contributes no column, where it previously wrote its measure under
|
|
204
|
+
`''`.
|
|
205
|
+
|
|
206
|
+
`null` and `''` are two different groups with two series, following
|
|
207
|
+
objectui#4508's ruling on the first dimension.
|
|
208
|
+
|
|
209
|
+
**The series key is collision-safe, not merely unlikely to collide.** Unlike an
|
|
210
|
+
axis bucket's private map key, a series key is a column of the emitted row and
|
|
211
|
+
the `dataKey` a renderer binds to, so it has to be unique within that row. A
|
|
212
|
+
group keys its column by its own display label — leaving an ordinary pivot's
|
|
213
|
+
rows, series, legend, tooltip and drill title exactly as they were — unless
|
|
214
|
+
that label cannot name it: shared with another group (a stored value spelling
|
|
215
|
+
the null bucket's label), reserved by the row itself (the x-axis column, the
|
|
216
|
+
identity carrier), or equal to some group's identity. Those key by identity
|
|
217
|
+
instead, which no other group has. Because no surviving label is any group's
|
|
218
|
+
identity, the two key spaces cannot meet.
|
|
219
|
+
|
|
220
|
+
Drill-through follows the same assignment: a clicked series key resolves back
|
|
221
|
+
to the group IDENTITY it names, and rows are matched on that. The previous
|
|
222
|
+
`String(r[gDim] ?? '')` comparison was the display-string matching
|
|
223
|
+
objectui#4508 removed on the x-axis — it spelled a null group and an
|
|
224
|
+
empty-string group alike, so the empty-string group's segment resolved to the
|
|
225
|
+
null group's records rather than its own.
|
|
226
|
+
|
|
227
|
+
No renderer change was needed: the null group's series carries the same
|
|
228
|
+
`nullCategoryLabel` the renderers already pass for the first dimension.
|
|
229
|
+
- 9ce096f: Give a chart bucket an identity distinct from its display label
|
|
230
|
+
|
|
231
|
+
objectui#4508. `buildChartSeries` used the bucket's DISPLAY string as the
|
|
232
|
+
bucket's own key, so two pairs of genuinely different groups were conflated —
|
|
233
|
+
and the segment click that drills a bar back to its records inherited both
|
|
234
|
+
conflations. The maintainer ruling (2026-08-14) approved the sentinel-identity
|
|
235
|
+
direction, aligning the chart branch with the distinct-bucket-id form the pivot
|
|
236
|
+
TABLE (`buildPivot`) already uses over the same dataset rows.
|
|
237
|
+
|
|
238
|
+
Two collisions, one cause:
|
|
239
|
+
|
|
240
|
+
- **A null group and an empty-string group drew ONE bar.** The pivot branch
|
|
241
|
+
keyed buckets by `String(xRaw ?? '')`, which spells `null` and `''`
|
|
242
|
+
identically. The bar took its label from whichever row created the bucket, and
|
|
243
|
+
the other group's segment then resolved to no row at all — a visible bar whose
|
|
244
|
+
click did nothing.
|
|
245
|
+
- **A record whose stored value spells the bucket label stole the null bucket's
|
|
246
|
+
drill.** A row storing the literal text `(None)` (or any localized
|
|
247
|
+
`chart.nullCategory` — `(未指定)` and the other nine packs) kept its own
|
|
248
|
+
bucket, so two bars carried the same axis text and BOTH resolved to the first.
|
|
249
|
+
That one is a wrong drill, not a dead one: clicking the null bucket's bar
|
|
250
|
+
opened the drawer on another group's records.
|
|
251
|
+
|
|
252
|
+
What changed:
|
|
253
|
+
|
|
254
|
+
- **`chartBucketId`** (`@object-ui/core`) is the bucket identity — the SAME
|
|
255
|
+
encoder `buildPivot` keys its buckets with (`pivotBucketId` over
|
|
256
|
+
`pivotDimensionValue`), so the two surfaces stop answering one question two
|
|
257
|
+
ways. The pivot branch now buckets by it, which is what makes null and `''`
|
|
258
|
+
two groups again.
|
|
259
|
+
- **`CHART_BUCKET_ID_KEY`** carries that identity on an emitted row, written
|
|
260
|
+
exactly where two DISTINCT buckets paint the same axis text — the complete set
|
|
261
|
+
of cases where the display string cannot name what was clicked. An ordinary
|
|
262
|
+
chart's rows are returned untouched (by identity), so no renderer-internal key
|
|
263
|
+
reaches an authoring surface.
|
|
264
|
+
- **`findChartSeriesRow`** takes that identity back as `options.bucketId` and
|
|
265
|
+
treats it as authoritative. The renderers forward it: the drill event gains
|
|
266
|
+
`categoryId` (`ChartSegmentClickEvent`, now declared once in
|
|
267
|
+
`@object-ui/core` instead of inline in three packages), `AdvancedChartImpl`
|
|
268
|
+
reads it off the clicked row on the cartesian, pie and funnel paths, and
|
|
269
|
+
`DatasetWidget.handleChartDrill` hands it to the lookup.
|
|
270
|
+
|
|
271
|
+
Behaviour change worth noting: an empty-string category no longer resolves to a
|
|
272
|
+
null-valued row. That tolerance was justified as the drill layer's own spelling
|
|
273
|
+
of "no group value", but no producer of this lookup's `category` writes it,
|
|
274
|
+
while `''` IS the axis text a genuine empty-string group paints — so the
|
|
275
|
+
tolerance was giving that group's bar a different group's records. A host that
|
|
276
|
+
forwards no `categoryId` keeps its existing drill unchanged.
|
|
277
|
+
- e05db88: A clicked cartesian mark names its own series, and the drill title reads its label
|
|
278
|
+
|
|
279
|
+
objectui#4672, objectui#4682.
|
|
280
|
+
|
|
281
|
+
**The dead pivoted drill.** objectui#4680 fixed what a cartesian click could
|
|
282
|
+
read out of recharts 3's `MouseHandlerDataParam`, and measured the wall it could
|
|
283
|
+
not get past: a chart-level click is an AXIS interaction, and recharts
|
|
284
|
+
dispatches those with `activeDataKey` hard-coded `undefined`, because the shared
|
|
285
|
+
cursor spans every series at that tick. A pivoted dataset chart — 2 dimensions,
|
|
286
|
+
1 measure, the shape ADR-0021 introduced — needs the series to resolve its drill
|
|
287
|
+
row, so every segment of every such dashboard chart stayed a dead click. The
|
|
288
|
+
series was left unresolved rather than guessed, and the card carried the rest.
|
|
289
|
+
|
|
290
|
+
The answer is the mark itself. This renderer draws the `Bar` / `Line` / `Area`,
|
|
291
|
+
so an item-level `onClick` closes over the very `dataKey` it was rendered with —
|
|
292
|
+
the series is statically known, not inferred from tooltip state.
|
|
293
|
+
|
|
294
|
+
Both handlers fire for one gesture (measured: item first, chart second, sharing
|
|
295
|
+
one `nativeEvent` object), so the item handler does not emit. It RECORDS its
|
|
296
|
+
series, stamped with that gesture, and the chart-level handler composes the one
|
|
297
|
+
event. That is the double-fire answer and the additive property together:
|
|
298
|
+
|
|
299
|
+
- **one click, one drill event**, because there is one emit site — not a second
|
|
300
|
+
event suppressed after the fact;
|
|
301
|
+
- **a click that lands on no mark is untouched**: it records nothing and falls
|
|
302
|
+
through to the objectui#4680 axis answer exactly as shipped — category, bucket
|
|
303
|
+
identity, and the series only where one series is plotted. Empty plot area
|
|
304
|
+
stays category-only, and "drill the whole category" was rejected as a
|
|
305
|
+
different product question. Nothing that resolved before stops resolving; a
|
|
306
|
+
line's `dot={false}` stroke simply GAINS the exact series where it is hit;
|
|
307
|
+
- pairing on the shared DOM event rather than on a flag means a record left by
|
|
308
|
+
one gesture can never be adopted by a later click.
|
|
309
|
+
|
|
310
|
+
The clicked key is forwarded exactly as rendered, `''` included: the
|
|
311
|
+
empty-string second-dimension group draws its own bar since objectui#4673, and
|
|
312
|
+
`''` is falsy, so a truthiness test on the way out would send no series at all
|
|
313
|
+
and leave that bar's drill standing on the reader's coercion instead of on what
|
|
314
|
+
was clicked.
|
|
315
|
+
|
|
316
|
+
**The opaque drill title.** `ChartSegmentClickEvent` gains `seriesLabel`, and
|
|
317
|
+
`DatasetWidget`'s drill drawer titles itself from `seriesLabel ?? series`.
|
|
318
|
+
`ev.series` stays the LOOKUP key — `findChartSeriesRow` resolves it through the
|
|
319
|
+
same assignment `buildChartSeries` made — and only the title reads the label.
|
|
320
|
+
|
|
321
|
+
The two strings are equal for every ordinary group, which is why reading the key
|
|
322
|
+
as a title went unnoticed. They part company when a group's label cannot name
|
|
323
|
+
it: the null bucket beside a record whose stored value literally spells
|
|
324
|
+
`(None)`, which is objectui#4508's collision on the series axis, reachable since
|
|
325
|
+
objectui#4673. Both groups then key by `chartBucketId`, and the drawer opened on
|
|
326
|
+
the right records under the title `Backlog / [null]`. An internal id where a
|
|
327
|
+
label belongs reads as broken DATA rather than as a broken title.
|
|
328
|
+
|
|
329
|
+
Neither string can do the other's job, which is why this is a second field
|
|
330
|
+
rather than a change to the first: the label is not resolvable (it is exactly
|
|
331
|
+
what the colliding groups share) and the key is not showable. `seriesLabel` is
|
|
332
|
+
optional and absent wherever a renderer resolved no label, so every other
|
|
333
|
+
chart's title is byte-identical.
|
|
334
|
+
- d2ce342: Retire the structured `confirm` object on actions (objectui#4314, maintainer ruling
|
|
335
|
+
2026-08-17, ADR-0049 enforce-or-remove). `confirmText` is now the one confirm
|
|
336
|
+
spelling — the only one the translation bundle can address
|
|
337
|
+
(`{ns}.objects.{obj}._actions.{name}.confirmText`), matching `@objectstack/spec`'s
|
|
338
|
+
action surface.
|
|
339
|
+
|
|
340
|
+
Breaking semantics (flagged `minor` per this repo's version-alignment policy):
|
|
341
|
+
|
|
342
|
+
- `@object-ui/types`: `ActionSchema.confirm` is a `?: never` tombstone — authoring
|
|
343
|
+
it is now a tsc error, and the Zod twin rejects any authored value at parse time
|
|
344
|
+
(it previously accepted the object). The backwards `@deprecated` note that
|
|
345
|
+
steered authors from `confirmText` INTO the structured arm is gone.
|
|
346
|
+
- `@object-ui/core`: `ActionRunner` no longer reads `confirm.message` (which used
|
|
347
|
+
to outrank `confirmText`, untranslated). `ActionDef.confirm` carries the same
|
|
348
|
+
`never` tombstone. The `ConfirmationHandler` signature is unchanged, but the
|
|
349
|
+
runner now invokes it without the `options` argument.
|
|
350
|
+
- `@object-ui/plugin-grid`: `resolveBulkActions` no longer falls back to
|
|
351
|
+
`confirm.message` when promoting an object action — spec metadata can never
|
|
352
|
+
deliver that key.
|
|
353
|
+
|
|
354
|
+
Nothing in the repo, the example apps, or the schema catalog authored the
|
|
355
|
+
structured form (verified on the issue); a dialog authored that way silently lost
|
|
356
|
+
localization. Reopen condition recorded on objectui#4314: real demand returns the
|
|
357
|
+
arm WITH bundle keys designed in.
|
|
358
|
+
|
|
359
|
+
### Patch Changes
|
|
360
|
+
|
|
361
|
+
- 2533ec5: The bulk-action dialog's "this widget needs a DataSource" rule now derives from the shared reference-field family instead of a fourth private copy.
|
|
362
|
+
|
|
363
|
+
`packages/plugin-grid/src/components/bulkParamToField.ts` held its own
|
|
364
|
+
`DATA_SOURCE_WIDGET_TYPES` — the fourth hand-maintained answer to one question
|
|
365
|
+
("which widget has to query records, so it must be handed a DataSource and a
|
|
366
|
+
`reference_to`"), and the only one whose member set matched none of the other
|
|
367
|
+
three: `lookup` / `master_detail` / `user`, against `@object-ui/core`'s
|
|
368
|
+
`EXPANDABLE_FIELD_TYPES` (which also holds `tree`) and the object form's rule
|
|
369
|
+
(which adds three widget-hint pickers). Nothing anywhere could detect the drift;
|
|
370
|
+
the same shape objectui#4770 and objectui#4790 each closed on another surface.
|
|
371
|
+
|
|
372
|
+
It now reads core's set through one predicate, so all three consumers of the rule
|
|
373
|
+
— the label prefetch / option source (`isLookupishParam`), the `dataSource` prop
|
|
374
|
+
the dialog threads into the widget (`fieldNeedsDataSource`), and the
|
|
375
|
+
`reference_to` / `display_field` branch of `bulkParamToField` — cannot drift apart
|
|
376
|
+
from each other or from the form again.
|
|
377
|
+
|
|
378
|
+
No behaviour change on any reachable path, which is why this is a patch. The one
|
|
379
|
+
member the two tables differed on, `tree`, can never be a widget key on this
|
|
380
|
+
surface: it is absent from the fields widget map and `mapFieldTypeToFormType`
|
|
381
|
+
sends it to `field:lookup`, so a `tree` param arrives at the rule as `lookup`
|
|
382
|
+
(pinned). The divergence in the other direction is deliberately preserved: the
|
|
383
|
+
form additionally wires `object-ref` / `filter-condition` / `recipient-picker`,
|
|
384
|
+
widget hints no object schema can declare and no bulk param produces — absorbing
|
|
385
|
+
them would change which widgets receive a DataSource here, which is a behaviour
|
|
386
|
+
change and not a convergence.
|
|
387
|
+
|
|
388
|
+
The pin is an identity pin, not a membership one: it spies on the `has` of the
|
|
389
|
+
Set object core exports, so a member-identical private copy fails it. A
|
|
390
|
+
value-equality assertion would have passed against exactly the defect this
|
|
391
|
+
change removes.
|
|
392
|
+
- bbe8b86: The allow-list of option widgets that are fed the live record is now one exported constant, `CASCADE_OPTION_WIDGET_TYPES`, instead of three private copies.
|
|
393
|
+
|
|
394
|
+
`select` / `multiselect` / `radio` / `checkboxes` are the widgets whose OFFERED
|
|
395
|
+
option set is re-resolved against a record (per-option `visibleWhen`, plus the
|
|
396
|
+
`dependsOn` gate), so they are the widgets a surface must thread its live record
|
|
397
|
+
to. Three surfaces feed that one evaluator — the object form, the single-record
|
|
398
|
+
action dialog and the bulk action dialog — and until now each carried its own
|
|
399
|
+
private `new Set([...])` of the same four keys, with a comment in each asking the
|
|
400
|
+
next person to change all three together. Nothing could have reported them
|
|
401
|
+
drifting: every copy passed its own behavioural tests, and a divergence would
|
|
402
|
+
have shown up only as one surface silently disagreeing with another about what
|
|
403
|
+
"the record" is.
|
|
404
|
+
|
|
405
|
+
The set now lives in `@object-ui/core`, next to `resolveCascadingOptions` — the
|
|
406
|
+
evaluator that reads that record — because core is the one package all three
|
|
407
|
+
surfaces already depend on, and it is re-exported from `@object-ui/fields` next
|
|
408
|
+
to `resolveFormWidgetType`, whose output is the vocabulary the keys are written
|
|
409
|
+
in. Both are the same object, pinned by test; each consumer keeps its own
|
|
410
|
+
normalization (`normalizeFieldType` in the form, `resolveFormWidgetType` in the
|
|
411
|
+
dialogs), which agree on these four members.
|
|
412
|
+
|
|
413
|
+
No behaviour changes: the members are identical on all three surfaces, and the
|
|
414
|
+
existing pins for each surface still assert the same records reaching the same
|
|
415
|
+
widgets. The rationale that was repeated in the three copies — including the
|
|
416
|
+
note that the widget-hint picker family (`filter-condition`, `recipient-picker`,
|
|
417
|
+
the lookup family) reads a different sibling key off the same channel and is
|
|
418
|
+
deliberately NOT in this set — is now stated once, in the constant's own
|
|
419
|
+
documentation. Whether the action and bulk dialogs should ever feed those
|
|
420
|
+
pickers stays an open question (objectui#4771), unchanged by this convergence.
|
|
421
|
+
- 8477be5: `cloneAsOverride()` now returns `DeepMutable<T>`, so a Tenant/User override draft type-checks as the mutable value it has always been.
|
|
422
|
+
|
|
423
|
+
`cloneAsOverride<T>(view: T): T` handed the input type straight back. Cloning a
|
|
424
|
+
`SystemView<S>` — which is `DeepReadonly<S>` plus the marker symbol — therefore
|
|
425
|
+
returned something still typed deep-readonly, even though the implementation has
|
|
426
|
+
always produced a plain mutable object (`structuredClone`, or a JSON round-trip
|
|
427
|
+
fallback) and deliberately drops the marker. The declaration was simply wrong
|
|
428
|
+
about its own value, and the documented override flow was the thing that broke:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
const draft = cloneAsOverride(userListView)
|
|
432
|
+
draft.columns.push({ name: 'name' }) // TS2339 before this change
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
That is `packages/core/README.md`'s override example, and it failed to compile
|
|
436
|
+
against the built types — measured by the doc-snippet compile gate, which reads
|
|
437
|
+
`dist/*.d.ts` rather than source. The neighbouring line one block up,
|
|
438
|
+
`userListView.columns.push(...) // ❌ TypeError (strict mode)`, is the opposite
|
|
439
|
+
demonstration and correctly still fails; only the draft line changes colour.
|
|
440
|
+
|
|
441
|
+
The fix adds `DeepMutable<T>`, the inverse of the `DeepReadonly<T>` that
|
|
442
|
+
`SystemView` is built from, and returns it. Per the maintainer's 2026-08-19
|
|
443
|
+
ruling (option A on objectui#5257), the alternatives were rejected by name:
|
|
444
|
+
teaching a cast in the README is the lenient-consumer pattern the contract rules
|
|
445
|
+
out, and declaring the block a documentation fragment hides a real signature
|
|
446
|
+
defect behind the fragment marker.
|
|
447
|
+
|
|
448
|
+
Why this is a patch and not a break: the return type relaxes TOWARD what the
|
|
449
|
+
runtime already does, never away from it. A caller gains permission to mutate;
|
|
450
|
+
nobody loses one. `DeepMutable<S>` stays assignable everywhere `DeepReadonly<S>`
|
|
451
|
+
or `SystemView<S>` was expected, so a caller who fed a draft back into a
|
|
452
|
+
deep-readonly position still compiles — both directions are pinned as type-level
|
|
453
|
+
cases in `freeze-schema.types.test.ts`. A repo-wide sweep found no call site at
|
|
454
|
+
all outside the README, so nothing in this workspace needed changing.
|
|
455
|
+
|
|
456
|
+
Two limits of `DeepMutable`, stated rather than discovered later. It is
|
|
457
|
+
symmetric with `DeepReadonly` arm for arm, which means it inherits the same
|
|
458
|
+
tuple behaviour: a tuple widens to an array, exactly as `DeepReadonly` widens it
|
|
459
|
+
in the other direction. And it does not remove the `SYSTEM_VIEW_MARKER` key —
|
|
460
|
+
the clone never carries the symbol at runtime, but the key is declared optional,
|
|
461
|
+
so keeping it states "may be absent", which is true. Excluding it would require
|
|
462
|
+
a non-homomorphic mapped type that drops the `?` modifier from every other
|
|
463
|
+
property and turns optional keys required — a strictly worse type traded for
|
|
464
|
+
removing a key that already reads as optional.
|
|
465
|
+
- ad07b65: Four packages stop publishing tooling material in their `dist/`
|
|
466
|
+
|
|
467
|
+
Each of these packages spelled its build exclusions as `*.test.*`, while this repo's tooling convention is a directory one — `__tests__` / `__mocks__` / `__benchmarks__`, exactly as `TOOLING_FILE` in `scripts/check-phantom-dependencies.mjs` spells it. Any tooling file whose *name* is not `*.test.*` therefore stayed in the emit program and shipped in the tarball. This is the same shape and the same cause as objectui#4006, which fixed `@object-ui/fields` and `@object-ui/plugin-editor` by the filename criterion and so did not reach these four.
|
|
468
|
+
|
|
469
|
+
Measured by building each package from a cleared `dist/` on both sides of the change. Nine files disappear, none appears, and every surviving file is untouched — the totals move by exactly the count removed:
|
|
470
|
+
|
|
471
|
+
| package | `dist/` files | removed |
|
|
472
|
+
| --- | --- | --- |
|
|
473
|
+
| `@object-ui/core` | 176 to 174 | `dist/__benchmarks__/core.bench.js`, `core.bench.d.ts` |
|
|
474
|
+
| `@object-ui/plugin-designer` | 70 to 66 | `dist/__tests__/__mocks__/plugin-form.d.ts`, `plugin-grid.d.ts`, and both `.d.ts.map` |
|
|
475
|
+
| `@object-ui/plugin-grid` | 62 to 60 | `dist/__tests__/explainDouble.d.ts` and its `.d.ts.map` |
|
|
476
|
+
| `@object-ui/plugin-view` | 13 to 12 | `dist/__tests__/explainDouble.d.ts` |
|
|
477
|
+
|
|
478
|
+
Only `@object-ui/core`'s had runtime weight. The other eight are declarations nothing resolves, but `core.bench.js` is a real emitted module whose first import is `import { bench, describe } from 'vitest'` — a runtime import of a package a consumer never installs, since `vitest` is a devDependency of `@object-ui/core` and devDependencies are not installed transitively. Nothing resolves it today either (it is not in the `exports` map), so no consumer breaks in either direction; this is the tarball shedding files nothing reached.
|
|
479
|
+
|
|
480
|
+
No type coverage leaves with the emit. The three plugins' helper and mock files are already program inputs of the `tsconfig.test.json` that each package's `type-check` chains, reached through the imports in the suites beside them — `tsc --listFiles` names all four files on both sides of the change. `core.bench.ts` had no such edge, since nothing imports a benchmark, so it is now named explicitly in `packages/core/tsconfig.test.json`. That move was deliberate rather than forced: `scripts/check-type-check-coverage.mjs` enumerates `*.test.ts(x)` only, so a benchmark that no program reads is invisible to it, and dropping the coverage silently would have been the "coverage that was right by accident" objectui#4006 recorded. Verified by appending a provably-false annotation to the benchmark, which turns `tsc -p packages/core/tsconfig.test.json` red at exit 2.
|
|
481
|
+
- 41f498b: Fix `packages/core/README.md`'s Component Registry example, which taught
|
|
482
|
+
`new ComponentRegistry()` against an exported singleton **instance**, not a
|
|
483
|
+
class — the built `packages/core/dist/registry/Registry.d.ts` declares
|
|
484
|
+
`export declare const ComponentRegistry: Registry<any>`, so the snippet did
|
|
485
|
+
not compile (`TS2351: This expression is not constructable`, measured by the
|
|
486
|
+
objectui#5138 doc-snippet type gate). A reader who copied it got a compile
|
|
487
|
+
error; if `new ComponentRegistry()` had compiled it would have produced a
|
|
488
|
+
second, empty registry nothing renders from, the more expensive half of the
|
|
489
|
+
mistake.
|
|
490
|
+
|
|
491
|
+
The snippet now calls `ComponentRegistry.register(...)` /
|
|
492
|
+
`ComponentRegistry.get(...)` directly on the singleton, with one line stating
|
|
493
|
+
it is the process-level shared instance `SchemaRenderer` resolves every
|
|
494
|
+
`type` against — the same wording `packages/components/README.md` was given
|
|
495
|
+
in objectui#5160, kept consistent across both READMEs. Readers who want their
|
|
496
|
+
own isolated registry still have `Registry` itself, separately exported as a
|
|
497
|
+
real class.
|
|
498
|
+
|
|
499
|
+
`scripts/check-doc-snippet-types.mjs`'s `UNGATED_DOCS` entry for
|
|
500
|
+
`packages/core/README.md` is updated to match: `TS2351x1` is dropped from its
|
|
501
|
+
reason text now that the diagnostic is gone. The entry is not deleted — the
|
|
502
|
+
document's remaining `TS2339x2` pair (a different, pre-existing defect) is
|
|
503
|
+
out of scope for this change; it's tracked as objectui#5257.
|
|
504
|
+
- ac600e5: A `user` field in a form now receives `dataSource` / `dependentValues` / `dependsOnLabels`, like every other reference field.
|
|
505
|
+
|
|
506
|
+
The form renderer decided which registered widget gets those three props from a
|
|
507
|
+
module-private `DATA_SOURCE_FIELD_TYPES` set, while `@object-ui/core` kept
|
|
508
|
+
`EXPANDABLE_FIELD_TYPES` for the same underlying fact — a field whose stored
|
|
509
|
+
value is a foreign key into another object. The core side's TSDoc claimed to
|
|
510
|
+
mirror the form's set, and it did for 15 days: the form's copy then gained
|
|
511
|
+
`capability-multiselect` (objectui#2403) and the three widget-hint pickers
|
|
512
|
+
`object-ref` / `filter-condition` / `recipient-picker` (objectui#2421) on the
|
|
513
|
+
same day, after which the two sets were not in a subset relation in either
|
|
514
|
+
direction — `user` only in core, the picker names only in the form — with
|
|
515
|
+
nothing able to report it.
|
|
516
|
+
|
|
517
|
+
The form now derives its rule instead of restating it: the reference half is
|
|
518
|
+
core's set, the form-specific half is the three picker names, which are widget
|
|
519
|
+
hints and can never be a declarable field `type`. Adding a member to
|
|
520
|
+
`EXPANDABLE_FIELD_TYPES` therefore also grants it the form's data-source wiring;
|
|
521
|
+
that coupling is intended and is now written down on both sides.
|
|
522
|
+
|
|
523
|
+
The user-visible half is `user`. It previously received none of the three props.
|
|
524
|
+
`dataSource` and `dependentValues` each have a `SchemaRendererContext` fallback
|
|
525
|
+
inside the widget, so the person picker limped along wherever a provider
|
|
526
|
+
happened to supply one; `dependsOnLabels` has no fallback, so a
|
|
527
|
+
dependency-gated user picker interpolated the raw API name into its
|
|
528
|
+
"select ... first" hint in every locale — the leak objectstack#5407 closed for
|
|
529
|
+
lookups and left open here. The widget contract's own `dataSource` doc has
|
|
530
|
+
always named `user` among the types the form renderer injects for.
|
|
531
|
+
|
|
532
|
+
No change to what is expanded, projected or rendered anywhere else: the core
|
|
533
|
+
set's members are untouched.
|
|
534
|
+
- c1ef923: Grid and related-list column headers no longer offer a sort on a `formula` column.
|
|
535
|
+
|
|
536
|
+
A `formula` value is computed on read: no driver materialises a column for it, so
|
|
537
|
+
a server `$orderby` naming one has nothing to order by. That sort never worked.
|
|
538
|
+
Until objectstack#6994 the platform did not say so — the response carried the very
|
|
539
|
+
values it had been asked to order by, out of order, under a `200`, with ascending
|
|
540
|
+
and descending byte-identical on a real SQL driver — and it now answers
|
|
541
|
+
`400 INVALID_SORT`. So the header was wrong before the platform's refusal and is
|
|
542
|
+
wrong after it, for the same reason: it offers a sort that cannot be performed.
|
|
543
|
+
|
|
544
|
+
`ObjectGrid` withheld the affordance only from reference-bearing columns
|
|
545
|
+
(objectui#3096). Unmaterialized types are a SECOND reason a server sort is
|
|
546
|
+
impossible, not a different mechanism, so it now reads both — and so do the two
|
|
547
|
+
sort entry points of a related list (the embedded table's headers and the
|
|
548
|
+
sort-button row a `data-list` card keeps), which each derived that rule
|
|
549
|
+
separately.
|
|
550
|
+
|
|
551
|
+
Client-side sorting is deliberately unchanged. There the rows are all in the
|
|
552
|
+
browser and the formula value is the one the server hydrated on read, so ordering
|
|
553
|
+
by what the cell shows is honest — the same split the relational carve-out makes.
|
|
554
|
+
A sort DECLARED in view metadata is also unchanged: it still goes out and is still
|
|
555
|
+
refused by name, because silently dropping an author's declaration would hide the
|
|
556
|
+
authoring error instead of surfacing it (the toolbar's sort picker keeps such a
|
|
557
|
+
field listed for exactly that reason — it is the only way to remove it).
|
|
558
|
+
|
|
559
|
+
The membership — `formula` alone — moved out of a private set in `ListView` into
|
|
560
|
+
`@object-ui/core` (`UNMATERIALIZED_FIELD_TYPES` / `isUnmaterializedFieldType`),
|
|
561
|
+
bound to `@objectstack/spec`'s own storage predicate so the renderer cannot drift
|
|
562
|
+
from what the drivers actually store. It is deliberately narrower than the spec's
|
|
563
|
+
write contract `COMPUTED_VALUE_TYPES`: a `summary` and an `autonumber` each get a
|
|
564
|
+
real maintained column and sort correctly, and withholding their headers would
|
|
565
|
+
have broken two affordances that work.
|
|
566
|
+
- af5e292: Emit explicit file extensions on relative import specifiers, so the published
|
|
567
|
+
entries can be imported by Node's own ESM resolver.
|
|
568
|
+
|
|
569
|
+
`@object-ui/react`'s built entry re-exported through extensionless relative
|
|
570
|
+
specifiers (`export * from './SchemaRenderer'`). Node does not extension-search
|
|
571
|
+
relative specifiers, so `import('@object-ui/react')` under plain Node — an SSR
|
|
572
|
+
host, or any consumer without a bundler — failed with `ERR_MODULE_NOT_FOUND`.
|
|
573
|
+
Bundled consumers were never affected and are unchanged by this.
|
|
574
|
+
|
|
575
|
+
`@object-ui/types`, `@object-ui/core` and `@object-ui/i18n` carried the same
|
|
576
|
+
emission; `@object-ui/react`'s entry stayed unloadable until they were fixed
|
|
577
|
+
too, because evaluation crosses into them. No exported API changed.
|
|
578
|
+
- 9f23d2b: The object list's Import button now honours `userActions.import`'s CEL predicates.
|
|
579
|
+
|
|
580
|
+
`@objectstack/spec@17.0.0` widened BOTH toolbar-scope keys, not just `create`:
|
|
581
|
+
`userActions.create` and `userActions.import` are typed identically
|
|
582
|
+
(`z.union([z.boolean(), RowCrudActionOverrideSchema])`) and
|
|
583
|
+
`resolveCrudAffordances` emits a predicate envelope for each, with the docblock
|
|
584
|
+
binding them in one breath ("`importPredicates` — same binding as
|
|
585
|
+
`createPredicates`"). objectui#4646 gave the `create` half a consumer and left
|
|
586
|
+
the `import` half declared-and-inert: `importPredicates` had zero readers in
|
|
587
|
+
objectui. An author could write `userActions.import.visibleWhen`, have the spec
|
|
588
|
+
accept it and the resolver parse it, and watch the object-list toolbar offer the
|
|
589
|
+
CSV import wizard unconditionally.
|
|
590
|
+
|
|
591
|
+
The toolbar now evaluates them, mirroring the related list's create half:
|
|
592
|
+
`visibleWhen` fails CLOSED (an unevaluable predicate hides the entry and warns
|
|
593
|
+
once), `disabledWhen` fails SOFT (an unevaluable one leaves the button enabled),
|
|
594
|
+
and the declared-ness rules are the family's — `?? true` for `visibleWhen` so
|
|
595
|
+
`visibleWhen: false` hides rather than reading as "ungated", `!= null` for
|
|
596
|
+
`disabledWhen` so an empty predicate reads as "no condition". The layer sits on
|
|
597
|
+
TOP of the object-level verdict: a predicate can narrow what the `managedBy`
|
|
598
|
+
bucket, the server's effective API operations and the principal's grant already
|
|
599
|
+
allow, never re-open what they closed.
|
|
600
|
+
|
|
601
|
+
Per the spec's binding, a toolbar predicate evaluates once per toolbar against
|
|
602
|
+
the record of the scope the toolbar sits in — and a standalone object list has
|
|
603
|
+
no record in scope. Predicates over the host scope (`os.user.*`, `features.*`)
|
|
604
|
+
are the meaningful shape there; one reading `record.*` has nothing to bind and
|
|
605
|
+
hides the entry, which is the fail-closed rule the spec spells out for exactly
|
|
606
|
+
this surface.
|
|
607
|
+
|
|
608
|
+
`UserActionsOverride.import` widens from `boolean` to the same union as
|
|
609
|
+
`create`, deliberately in this same change: objectui#4646 kept it narrow on
|
|
610
|
+
purpose because widening a type ahead of its consumer re-declares the
|
|
611
|
+
inert-metadata defect one key over. Type and consumer travel together.
|
|
612
|
+
- 31676be: `buildChartSeries`' pivot branch preserves key ABSENCE, so a never-projected first dimension still reaches the framework#4033 placeholder.
|
|
613
|
+
|
|
614
|
+
`hasNoCategoryKey` (plugin-charts' `AdvancedChartImpl`) exists to catch one
|
|
615
|
+
shape: a dimension a dataset query GROUPED BY but never PROJECTED, so no row
|
|
616
|
+
carries the category key. Rather than draw an axis with no marks, the renderer
|
|
617
|
+
names the missing key. Its whole signal is `key in row`, asked of the rows it is
|
|
618
|
+
handed — which for a dataset-bound chart are `buildChartSeries`' output.
|
|
619
|
+
|
|
620
|
+
The pivot branch wrote `[xKey]` onto every bucket it created, so `key in row`
|
|
621
|
+
was unconditionally true downstream and the guard could not fire for a
|
|
622
|
+
2-dimension/1-measure chart no matter what the query returned. Every row
|
|
623
|
+
collapsed into one unnamed bucket drawing a blank tick — the exact silent shape
|
|
624
|
+
the placeholder was introduced to eliminate. The defence was dead precisely
|
|
625
|
+
where the defect it guards against lands. The single-dimension branch was never
|
|
626
|
+
affected: it passes rows through, so key-absent rows stay key-absent.
|
|
627
|
+
|
|
628
|
+
An emitted bucket now carries the axis key exactly when some row of it did.
|
|
629
|
+
That is `hasNoCategoryKey`'s own `rows.some(key in row)` lifted through the
|
|
630
|
+
pivot's aggregation, so the guard reads the same fact before and after.
|
|
631
|
+
|
|
632
|
+
The route rests on a measurement of how a dataset query actually reports an
|
|
633
|
+
unprojected dimension in a 2-dimension grouping: it OMITS the key. The ObjectQL
|
|
634
|
+
strategy writes a dimension key only when the engine returned that column, the
|
|
635
|
+
native-SQL strategy returns driver rows carrying only the selected columns, JSON
|
|
636
|
+
cannot transport `undefined`, and `ObjectStackAdapter.queryDataset` passes rows
|
|
637
|
+
through by reference. An explicit `null` means the opposite — the column WAS
|
|
638
|
+
projected and its value is null — and that case is untouched: it still renders
|
|
639
|
+
under the `(None)` bucket label (objectui#4466 / objectui#4497). Where one
|
|
640
|
+
bucket collects both (an absent value and a stored `null` share the `[null]`
|
|
641
|
+
identity), the bucket keeps its label and draws, because refusing there would
|
|
642
|
+
tell an author their query never projected a dimension that it did.
|
|
643
|
+
|
|
644
|
+
No change to any chart whose first dimension projected: ordinary, null-valued
|
|
645
|
+
and empty-string categories all emit byte-identical rows, key order included.
|
|
646
|
+
- 5ffcc14: fix(plugin-report): forward the chart chrome and series presentation `ReportChartSchema` declares (objectui#4877)
|
|
647
|
+
|
|
648
|
+
A report's embedded chart forwarded exactly six keys to the registered chart
|
|
649
|
+
component — `chartType`, `data`, `height`, `isAnimationActive`, `series`,
|
|
650
|
+
`xAxisKey`. Everything else `ReportChartSchema` declares as authorable never
|
|
651
|
+
left the report renderer, so it was inert metadata: the author writes it, the
|
|
652
|
+
schema accepts it, nothing reads it.
|
|
653
|
+
|
|
654
|
+
`showLegend` was the sharpest case because dropping it does not merely ignore
|
|
655
|
+
the author, it INVERTS them: `AdvancedChartImpl` computes
|
|
656
|
+
`legendVisible = showLegend !== false`, so an absent value means the legend is
|
|
657
|
+
on and an explicit `showLegend: false` still drew one.
|
|
658
|
+
|
|
659
|
+
Now lowered, under objectui#4229's ruled data/presentation split:
|
|
660
|
+
|
|
661
|
+
- chrome — `showLegend`, `showDataLabels`, `colors` (both the positional-palette
|
|
662
|
+
array and the per-category record), `subtitle`, `description`, `annotations`,
|
|
663
|
+
`interaction`, `height`;
|
|
664
|
+
- per-series presentation — `color`, `stack`, `type`, `yAxis`, `dashArray`,
|
|
665
|
+
`opacity`, `variant`, matched by `series[].name` so series MEMBERSHIP stays
|
|
666
|
+
with the dataset.
|
|
667
|
+
|
|
668
|
+
`title` is deliberately not forwarded: the report renderer paints it as its own
|
|
669
|
+
heading above the plot, and forwarding it would draw a second one inside the
|
|
670
|
+
chart's frame. `aria` is not lowered either — nothing on this path reads it
|
|
671
|
+
(`AdvancedChartImpl` has no `aria` prop, and this renderer hands the component a
|
|
672
|
+
schema directly rather than through `SchemaRenderer`'s flat ARIA injection), so
|
|
673
|
+
forwarding it would move declared-but-unread one layer down.
|
|
674
|
+
|
|
675
|
+
The two helpers (`chartConfigPresentation`, `mergeAuthoredPresentation`) moved
|
|
676
|
+
from `plugin-dashboard`'s `DatasetWidget` to `@object-ui/core` beside
|
|
677
|
+
`buildChartSeries`, the derivation they merge onto, so both surfaces lower one
|
|
678
|
+
vocabulary once instead of keeping a second copy (the duplication objectui#4389
|
|
679
|
+
filed as a defect). `@object-ui/core` additionally exports `mergeAuthoredSeries`
|
|
680
|
+
— the series merge alone — for a surface whose axes are bare dimension/measure
|
|
681
|
+
NAME strings rather than spec `ChartAxis` objects, which is what a report chart
|
|
682
|
+
declares. `DatasetWidget` re-exports both names, so its public surface and its
|
|
683
|
+
rendering are unchanged.
|
|
684
|
+
- d971e51: A create form no longer deadlocks on a `requiredWhen` field that also declares a runtime `defaultValue`.
|
|
685
|
+
|
|
686
|
+
`#4069` ruled that in **create** mode a field whose `defaultValue` is a runtime
|
|
687
|
+
instruction the server resolves per insert (`NOW()` / `current_user`, or a CEL
|
|
688
|
+
Expression envelope) is producer-owned: the control is deliberately left empty
|
|
689
|
+
and the key is omitted from the payload, because `ObjectQL.applyFieldDefaults`
|
|
690
|
+
resolves the declaration only for a field that arrives absent or null. That was
|
|
691
|
+
implemented on the STATIC `required` flag.
|
|
692
|
+
|
|
693
|
+
The conditional spelling was not covered. `requiredWhen` is resolved one layer
|
|
694
|
+
downstream, in the form renderer, against the live record — so a predicate
|
|
695
|
+
resolving TRUE on a create form put the requirement straight back: the control
|
|
696
|
+
was still empty by design, the submit was refused, and the user had nothing
|
|
697
|
+
sensible to type.
|
|
698
|
+
|
|
699
|
+
Both spellings now behave identically on a producer-owned field. A
|
|
700
|
+
`requiredWhen` predicate is a claim about the value at rest in a given state,
|
|
701
|
+
and `NOW()` / `current_user` resolve at insert regardless of state, so the
|
|
702
|
+
producer's guarantee covers the conditional claim by the same argument that
|
|
703
|
+
covers the unconditional one. An author who really means "the user must supply
|
|
704
|
+
this in this state" has a natural spelling for it: do not declare the default.
|
|
705
|
+
|
|
706
|
+
The suppression lands in the single evaluator both layers read,
|
|
707
|
+
`resolveFieldRuleState` — the same verdict that draws the required marker and
|
|
708
|
+
the one the submit-time check consults — so a field can never lose its asterisk
|
|
709
|
+
while still refusing the write. The classifier that answers "is this value the
|
|
710
|
+
producer's to supply" moved down to `@object-ui/core`
|
|
711
|
+
(`isRuntimeDefault` / `isServerOwnedValue`, re-exported from
|
|
712
|
+
`@object-ui/plugin-form`) so the renderer, the wizard's cross-step gate and the
|
|
713
|
+
create-form field builders all read one implementation rather than three.
|
|
714
|
+
|
|
715
|
+
**Edit mode is unchanged.** Defaults do not re-apply to an existing record, so
|
|
716
|
+
on a persisted row the token was already resolved at insert and blanking the
|
|
717
|
+
column is a real removal: `requiredWhen` enforces there exactly as authored.
|
|
718
|
+
Fields with no declared default, and fields whose default is a static literal
|
|
719
|
+
(which IS seeded into the control), are also unaffected in both modes.
|
|
720
|
+
- dfc6975: Related-list "+ New" now honours `userActions.create` predicates, and the grid
|
|
721
|
+
toolbar's inline-edit affordance is gated on `update` permission (objectui#4646,
|
|
722
|
+
objectui#4647).
|
|
723
|
+
|
|
724
|
+
Two declared-but-unenforced gaps on the same toolbar surface.
|
|
725
|
+
|
|
726
|
+
**#4646 — `createPredicates` had a producer and no consumer.**
|
|
727
|
+
`@objectstack/spec@17.0.0` widened `userActions.create` to
|
|
728
|
+
`z.union([z.boolean(), RowCrudActionOverrideSchema])`, so `resolveCrudAffordances`
|
|
729
|
+
emits `createPredicates` — and nothing in objectui read them, against roughly
|
|
730
|
+
fifteen consumption sites apiece for `editPredicates` / `deletePredicates`. The
|
|
731
|
+
symptom: a parent record entering a frozen state correctly greyed its children's
|
|
732
|
+
row Edit/Delete while the related list's "+ New" stayed fully live, so the user
|
|
733
|
+
filled in the whole child form to earn a server 409. The related-list toolbar now
|
|
734
|
+
evaluates `visibleWhen` / `disabledWhen` **once against the host parent record**,
|
|
735
|
+
per the spec docblock's binding for this key, on top of the existing
|
|
736
|
+
`o.create ∧ can(child, 'create')` check. `visibleWhen` hides "+ New" and fails
|
|
737
|
+
CLOSED; `disabledWhen` greys it and fails SOFT — the same evaluator, fail
|
|
738
|
+
directions and hidden-vs-disabled split the record header already uses for
|
|
739
|
+
edit/delete (objectui#4419 / PR #4515). A bare-boolean `userActions.create` is
|
|
740
|
+
untouched: with no predicates there is nothing to evaluate.
|
|
741
|
+
|
|
742
|
+
**#4647 — the inline-edit toggle was the one ungated affordance on its toolbar.**
|
|
743
|
+
It rendered on "grid view ∧ the host wired `onInlineEditChange` ∧ not the compact
|
|
744
|
+
toolbar", and every host wires that callback unconditionally. New and Import are
|
|
745
|
+
hidden for an account without the grant and the bulk-delete entry on the same
|
|
746
|
+
toolbar ANDs `can(obj, 'delete')`, but a read-only principal could flip inline
|
|
747
|
+
edit, modify cells and press "Save all" to earn a server 403. It is now gated on
|
|
748
|
+
the object's resolved edit affordance ∧ `can(object, 'update')`, mirroring that
|
|
749
|
+
bulk-delete gate. The gate is applied at all three sites that carry this
|
|
750
|
+
affordance — the wide toolbar's toggle, the compact toolbar's settings-popover
|
|
751
|
+
entry (which previously had no gate at all, not even the callback), and the
|
|
752
|
+
`editable` mode handed to the grid, so a stored view carrying `inlineEdit: true`
|
|
753
|
+
can no longer drop a read-only principal into editable cells with no toggle to
|
|
754
|
+
press.
|
|
755
|
+
|
|
756
|
+
`ListViewSchema.userActions.editInline` is also consumed now: an explicit `false`
|
|
757
|
+
withholds the affordance wholesale, which authors previously could not do.
|
|
758
|
+
|
|
759
|
+
**Behaviour change for read-only users, stated plainly.** Where the UI used to
|
|
760
|
+
offer inline editing and let the server refuse it, it now declines to offer the
|
|
761
|
+
entry point at all. No data access changes — the server gate was and remains the
|
|
762
|
+
enforcement boundary; this only stops the UI walking users into round-trips
|
|
763
|
+
guaranteed to fail. Accounts *with* the grant see no change, and hosts with no
|
|
764
|
+
`PermissionProvider` mounted (standalone embeds, the Studio designer) keep
|
|
765
|
+
today's behaviour, since `can()` answers `true` there by design.
|
|
766
|
+
|
|
767
|
+
One deliberate non-change: the absent case of `userActions.editInline` defers to
|
|
768
|
+
the host's existing `inlineEdit` channel rather than enforcing the spec's
|
|
769
|
+
`.default(false)`. Enforcing that default would remove the toggle from every
|
|
770
|
+
stored console list view in one release, since nothing folds a legacy key into
|
|
771
|
+
`editInline` and no existing view declares it. This follows the rule the
|
|
772
|
+
surrounding toolbar-flag block already states for itself — defaults chosen to
|
|
773
|
+
match what the flags have always done. `InterfaceListPage`, the key's other
|
|
774
|
+
consumer, reads the absent case as OFF, because the ADR-0047 interface page has
|
|
775
|
+
no such host channel to defer to.
|
|
776
|
+
- Updated dependencies [88085e3]
|
|
777
|
+
- Updated dependencies [279fb13]
|
|
778
|
+
- Updated dependencies [1184192]
|
|
779
|
+
- Updated dependencies [a2a9747]
|
|
780
|
+
- Updated dependencies [af5e292]
|
|
781
|
+
- Updated dependencies [7f96b10]
|
|
782
|
+
- Updated dependencies [f1d4748]
|
|
783
|
+
- Updated dependencies [578e025]
|
|
784
|
+
- Updated dependencies [598c89a]
|
|
785
|
+
- Updated dependencies [b8b9af4]
|
|
786
|
+
- Updated dependencies [97abb24]
|
|
787
|
+
- Updated dependencies [deb157a]
|
|
788
|
+
- Updated dependencies [d2ce342]
|
|
789
|
+
- Updated dependencies [9695da7]
|
|
790
|
+
- Updated dependencies [58b8346]
|
|
791
|
+
- Updated dependencies [3cf4de0]
|
|
792
|
+
- Updated dependencies [c9dc811]
|
|
793
|
+
- Updated dependencies [a0b9e91]
|
|
794
|
+
- Updated dependencies [99bd015]
|
|
795
|
+
- @object-ui/types@17.6.0
|
|
796
|
+
|
|
3
797
|
## 17.5.0
|
|
4
798
|
|
|
5
799
|
### Minor Changes
|