@object-ui/core 17.2.0 → 17.5.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 +1436 -0
- package/README.md +31 -12
- package/dist/actions/ActionEngine.d.ts +53 -31
- package/dist/actions/ActionEngine.js +87 -66
- package/dist/actions/ActionRunner.d.ts +215 -24
- package/dist/actions/ActionRunner.js +334 -17
- package/dist/actions/actionErrorDetail.d.ts +26 -0
- package/dist/actions/actionErrorDetail.js +37 -0
- package/dist/actions/actionKeys.d.ts +96 -20
- package/dist/actions/actionKeys.js +168 -18
- package/dist/actions/actionResponse.d.ts +42 -0
- package/dist/actions/actionResponse.js +104 -0
- package/dist/actions/index.d.ts +3 -0
- package/dist/actions/index.js +3 -0
- package/dist/actions/serverActionHandler.d.ts +143 -0
- package/dist/actions/serverActionHandler.js +137 -0
- package/dist/data-scope/ViewDataProvider.d.ts +37 -12
- package/dist/data-scope/ViewDataProvider.js +83 -5
- package/dist/data-scope/element-data-source.d.ts +113 -0
- package/dist/data-scope/element-data-source.js +216 -0
- package/dist/data-scope/index.d.ts +2 -1
- package/dist/data-scope/index.js +1 -0
- package/dist/evaluator/ExpressionEvaluator.js +4 -2
- package/dist/evaluator/declaredPredicate.d.ts +119 -0
- package/dist/evaluator/declaredPredicate.js +152 -0
- package/dist/evaluator/fieldRules.d.ts +42 -2
- package/dist/evaluator/fieldRules.js +101 -11
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +18 -3
- package/dist/evaluator/listConditional.js +0 -0
- package/dist/evaluator/optionRules.js +5 -1
- package/dist/evaluator/predicateInput.d.ts +132 -0
- package/dist/evaluator/predicateInput.js +129 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +26 -1
- package/dist/protocols/DndProtocol.d.ts +42 -1
- package/dist/protocols/KeyboardProtocol.d.ts +31 -1
- package/dist/protocols/SharingProtocol.d.ts +11 -1
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +8 -1
- package/dist/registry/Registry.d.ts +24 -0
- package/dist/theme/ThemeEngine.d.ts +0 -8
- package/dist/theme/ThemeEngine.js +22 -103
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +7 -1
- package/dist/types/index.d.ts +42 -8
- package/dist/utils/chart-series.d.ts +258 -5
- package/dist/utils/chart-series.js +423 -20
- package/dist/utils/compare-to.d.ts +40 -13
- package/dist/utils/compare-to.js +4 -32
- package/dist/utils/dashboard-filters.d.ts +54 -8
- package/dist/utils/dashboard-filters.js +261 -12
- package/dist/utils/dataset-format.d.ts +79 -15
- package/dist/utils/dataset-format.js +130 -9
- package/dist/utils/dataset-pivot.d.ts +78 -0
- package/dist/utils/dataset-pivot.js +78 -0
- package/dist/utils/dom-props.d.ts +137 -0
- package/dist/utils/dom-props.js +153 -0
- package/dist/utils/filter-converter.d.ts +39 -11
- package/dist/utils/filter-converter.js +87 -7
- package/dist/utils/normalize-list-view.d.ts +26 -6
- package/dist/utils/normalize-list-view.js +30 -8
- package/dist/utils/number-display.d.ts +151 -0
- package/dist/utils/number-display.js +71 -0
- package/dist/utils/predicate-fields.d.ts +96 -0
- package/dist/utils/predicate-fields.js +163 -0
- package/dist/utils/predicate-record.d.ts +32 -0
- package/dist/utils/predicate-record.js +120 -0
- package/dist/utils/record-title.js +0 -0
- package/dist/utils/resolve-view-id.d.ts +31 -0
- package/dist/utils/resolve-view-id.js +45 -0
- package/dist/utils/sort-query.d.ts +55 -0
- package/dist/utils/sort-query.js +35 -0
- package/package.json +5 -5
- package/dist/protocols/NotificationProtocol.d.ts +0 -71
- package/dist/protocols/NotificationProtocol.js +0 -89
- package/dist/utils/merge-views-into-objects.d.ts +0 -19
- package/dist/utils/merge-views-into-objects.js +0 -39
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1431 @@
|
|
|
1
1
|
# @object-ui/core
|
|
2
2
|
|
|
3
|
+
## 17.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ee66e2e: Close `ActionDef` — delete the `[key: string]: any` index signature and converge `visible` / `disabled` on the spec's unified shape.
|
|
8
|
+
|
|
9
|
+
`ActionDef` accepted any key of any type, so a typo (`targt`) and a retired spec
|
|
10
|
+
key (`execute`) both type-checked and the runner then silently bound no handler
|
|
11
|
+
— the objectstack#2169 "Mark Done does nothing" shape. Step 1
|
|
12
|
+
(objectstack#4075) made that audible with a dev-mode warning; step 2 promoted
|
|
13
|
+
the 18 spec-owned keys to real fields. This is **step 3**, executing the
|
|
14
|
+
maintainer's 2026-08-06 ruling now that its upstream half shipped in
|
|
15
|
+
`@objectstack/spec` 17.0.0-rc.6 (objectstack#5970).
|
|
16
|
+
|
|
17
|
+
- **`visible` and `disabled` now have ONE shape, derived from the spec** —
|
|
18
|
+
`boolean | string(CEL) | { dialect, source }`. The ruling was "统一形状,spec
|
|
19
|
+
采纳": boolean is the degenerate literal verdict, the string is CEL shorthand,
|
|
20
|
+
the envelope is the full form. `visible` loses its hand-written `| boolean`
|
|
21
|
+
(the spec adopted that arm, so restating it locally would be a second
|
|
22
|
+
contract), and `disabled` gains the envelope arm it never had — it was
|
|
23
|
+
`string | boolean`, which is why the envelope the spec emits could only be
|
|
24
|
+
read through a cast.
|
|
25
|
+
- **The index signature is gone.** `tsc` now rejects an unknown or retired key
|
|
26
|
+
at any site that authors an action literal in code.
|
|
27
|
+
- **Five keys the deletion surfaced, promoted to real fields.** `to`,
|
|
28
|
+
`external`, `newTab`, `replace` — the `navigation` alias's own spelling, ruled
|
|
29
|
+
legitimate by step 1 and listed in `NAVIGATION_ALIAS_KEYS` ever since, but
|
|
30
|
+
declared only as data; and `description`, which every action renderer forwards
|
|
31
|
+
(`check:action-forward-parity` requires it) and the param-collection dialog
|
|
32
|
+
reads for its subtitle (objectui#4192). These were the only two `TS2353`s the
|
|
33
|
+
deletion produced across the whole workspace.
|
|
34
|
+
- **`ActionContext` keeps its index signature**, deliberately. It is a runtime
|
|
35
|
+
data bag whose keys are genuinely open; `ActionDef` is a declared metadata
|
|
36
|
+
contract. That asymmetry is the point, and it is now pinned in both
|
|
37
|
+
directions.
|
|
38
|
+
|
|
39
|
+
**Breaking edge, deliberate — same class as step 2's, one step further.** An
|
|
40
|
+
`ActionDef` literal carrying a key this interface does not declare is now a
|
|
41
|
+
compile error where it previously compiled and did nothing at runtime. That
|
|
42
|
+
includes the retired `execute` (rename it to `target`; `os migrate meta --from
|
|
43
|
+
16` rewrites it) and plain typos. Values that were only ever absorbed silently
|
|
44
|
+
are the ones that stop compiling, so the failure moves to where it can be fixed
|
|
45
|
+
rather than appearing as a button that does nothing.
|
|
46
|
+
|
|
47
|
+
**What did NOT retire with the index signature**, contrary to step 1's
|
|
48
|
+
expectation: the dev-mode `warnOnUnknownActionKeys` shim and `executeScript`'s
|
|
49
|
+
`execute` rename prescription both stay. `tsc` only ever sees actions authored
|
|
50
|
+
as TypeScript, while stored `sys_metadata` rows are rehydrated UNPARSED
|
|
51
|
+
(objectstack#3903) — which is the population `execute: 'markDone'` actually
|
|
52
|
+
lives in. The two mechanisms cover disjoint populations; retiring the runtime
|
|
53
|
+
half would have re-opened the gap it was written for.
|
|
54
|
+
|
|
55
|
+
- 3fc2971: A null-keyed group renders as an explicit bucket instead of silently vanishing from a chart (objectui#4466)
|
|
56
|
+
|
|
57
|
+
`buildChartSeries`' single-dimension branch passed rows through verbatim, so a row whose category VALUE is `null` reached recharts with a null category and drew no mark. The visible outcome was not an empty chart but a quietly wrong one: rows `[{user_id: null, event_count: 51}, {user_id: 'Dev Admin', event_count: 2}]` drew exactly ONE bar — the dominant group, 51 of 53 events, dropped while the y-axis scale still accommodated it, so the chart understated its own data and the axis proved the data had been there. With every group null it drew axes, gridlines and an axis title with zero marks and no empty state, which is the shipped first-boot state of the built-in System Overview board's "Events by User" (every seeded `sys_audit_log` row is written with `user_id = NULL`).
|
|
58
|
+
|
|
59
|
+
The mapping lives in the shared series layer, so dashboard widgets and standalone `ObjectChart` get one answer rather than a per-chart patch in the recharts wrapper. It resolves the two-answers disagreement the card names as well: an empty result set keeps the designed empty state, a non-empty result always draws bars — the null bucket included.
|
|
60
|
+
|
|
61
|
+
`@object-ui/core` gains `NULL_CATEGORY_LABEL` and `ChartSeriesOptions`; `buildChartSeries` and `findChartSeriesRow` each take an optional trailing `options`. Both additive — every existing call site compiles and behaves identically, and a result with no null category is still returned by array identity. The two helpers are a pair on purpose: the caller matches a clicked segment against rows that still carry the raw `null`, so `findChartSeriesRow` reads the bucket label back to that row and the newly-visible bar keeps its drill-through instead of resolving to `-1`.
|
|
62
|
+
|
|
63
|
+
The label goes through the i18n channel (`chart.nullCategory`, en `(None)` / zh `(未指定)`, all ten packs), passed down by the renderer: `@object-ui/core` is React-free and cannot read the locale bundle, so it takes the resolved string the same way `dimensionOptionTranslator` takes a resolver. Its English constant is the floor for a provider-less host, not the mechanism.
|
|
64
|
+
|
|
65
|
+
`hasNoCategoryKey` (framework#4033) is untouched and now documented against this: a row that does not carry the category key AT ALL is a different defect — a dimension grouped by but never projected — and keeps its explanatory placeholder. The bucket deliberately never ADDS the key to such a row, which is what keeps that guard's signal alive. Key absent → the placeholder; key present with a null value → the bucket.
|
|
66
|
+
|
|
67
|
+
- dde7283: `chatbot` and `chatbot-enhanced` now pass only whitelisted DOM props to their host element (objectui#4431)
|
|
68
|
+
|
|
69
|
+
Both registrations destructured `schema` and `className` and forwarded everything else. `SchemaRenderer` hands a registered component the authored node's own keys, the contents of its `props` container, the ARIA it resolved and the host's trailing props — so all of it became attributes on the chat root `div`, because React passes unknown lowercase attributes through in silence and stringifies object values. Measured through the real SDUI path with a data-source adapter attached: **14 non-DOM attributes on each widget**, including `datasource="[object Object]"` (the injected adapter, which only appears on a deployment that really loads data) and a camelCase `arialabel` sitting next to the resolved `aria-label`, so the element carried each ARIA value twice under two spellings — one of them meaningless to assistive technology.
|
|
70
|
+
|
|
71
|
+
Both are now consume-or-whitelist: configuration is read off `schema` as before, the evaluated `disabled` verdict is consumed by name, and only `toDomProps`' output reaches the element. The resolved `aria-label` / `aria-describedby`, `role`, `id`, `tabIndex` and the `data-*` family still arrive — dropping them would have been an accessibility regression dressed as a leak fix, so the pin asserts the delivered set exactly, not just the absent one. `chatbot-floating` is untouched: its content mounts through a portal and its root never spread.
|
|
72
|
+
|
|
73
|
+
`@object-ui/core` gains the shared executor this migration needs (`utils/dom-props.ts`): `toDomProps` for the SDUI widget contract, plus `pickDomProps` — the mechanism — for a package whose own contract declares a different key set. That is the objectui#4409 dependency direction: plugin packages declare `@object-ui/core` and must not grow a dependency on `@object-ui/fields` to reach a whitelist.
|
|
74
|
+
|
|
75
|
+
`@object-ui/fields` keeps its own key list and its compile-time bindings, and now executes them through core's mechanism. Its behaviour is unchanged and its exported `DomProps<P>` is the same structural type. The two lists differ for measured reasons and no longer can drift silently: `name` and `disabled` are legal only on form controls, which is what every field widget renders and what `FieldWidgetComponentProps` declares, while `role` is resolved by `SchemaRenderer` for every SDUI node and is not part of the field contract. A new assertion binds every shared key in both directions, with `role` named as the single deliberate exception.
|
|
76
|
+
|
|
77
|
+
- f279deb: fix(core): bare-string filter options — docs/examples stop teaching it, the runtime lift warns (objectui#4356)
|
|
78
|
+
|
|
79
|
+
`globalFilters[].options` had two de-facto contracts. `@objectstack/spec`'s `GlobalFilterSchema` accepts only `{ value, label }` pairs, while `normalizeFilterOptions` also lifted a bare-string shorthand (`options: ['EMEA', 'APAC']`) — so a dashboard authored that way rendered correctly in objectui and was refused the moment it reached the platform's validation. That is the "one strict contract beats N dialects" divergence AGENTS.md #0.1 names, with the renderer's tolerance hiding the producer's bug instead of surfacing it.
|
|
80
|
+
|
|
81
|
+
Maintainer ruling of 2026-08-12 on objectstack#7917, verbatim 「7917 ②」: **the spec stays strict; the runtime lift retires behind a deprecation window sized by a stored-dashboard survey.** This is Phases 0 and 1 of that window, shipped together. Phase 2 (removing the lift) is scheduled on objectstack#7917 and is deliberately not here.
|
|
82
|
+
|
|
83
|
+
**Phase 1 — the lift now says so out loud.** `normalizeFilterOptions` still lifts a bare string, unchanged and mechanically lossless (`'EMEA'` becomes `{ value: 'EMEA', label: 'EMEA' }`), because stored dashboards carry the shorthand and dropping it silently would turn a rendering filter into an empty one. It now also logs a deprecation warning naming the offending filter, quoting the offending values, and printing the canonical replacement. The warning fires **once per offending filter per session** — `resolveDashboardFilterDefs` runs on every dashboard render, and a warning that floods the console is a warning that gets muted — and it is dev-mode only, matching the `warnOnDeprecatedObjectParams` convention in `actions/actionKeys.ts`. It does not fire for canonical object options, and a mixed array names only its bare members, since partial migrations happen. A silent lift can never be retired, because nothing would ever show that the last shorthand document is gone (ADR-0078).
|
|
84
|
+
|
|
85
|
+
**Phase 0 — objectui stopped teaching the form.** The stored-dashboard survey on objectstack#7917 found the shorthand's source: objectui's own docs and its schema-catalog corpus — which the catalog's `package.json` declares an AI RAG/few-shot retrieval source — still authored it, so the stored population was still growing. All seven non-test occurrences are corrected to the pair form: `content/docs/guide/dashboard-filters.md` (a code block **and** a prose passage that presented the shorthand as an equal alternative), `content/docs/plugins/plugin-dashboard.mdx`, `packages/plugin-dashboard/README.md`, and the three `examples/schema-catalog` `filtered-dashboard*.json` entries. Warning authors while the docs still taught the form would have been a contradiction users report as a bug.
|
|
86
|
+
|
|
87
|
+
**Guardrail.** The schema catalog previously asserted only that its entries were structurally well-formed and rendered without throwing — which is exactly how a spec-invalid example got in. Every `globalFilters[]` entry in every `plugin-dashboard` catalog example is now parsed with the real `@objectstack/spec` `GlobalFilterSchema`, with a non-vacuity control so a broken sweep cannot read as green.
|
|
88
|
+
|
|
89
|
+
New export: `resetDashboardFilterWarnings()`, the warn-once memo reset, matching `resetActionKeyWarnings`. Graded `minor` for that additive export — measured, the emitted `.d.ts` gains exactly one declaration and narrows nothing.
|
|
90
|
+
|
|
91
|
+
- eb7f586: Dashboard dataset measures follow the display locale (objectui#4566).
|
|
92
|
+
|
|
93
|
+
`formatMeasure` and `formatDimensionValue` in `@object-ui/core` formatted every
|
|
94
|
+
value with a bare `undefined` locale tag at all three of their `Intl` sites.
|
|
95
|
+
`undefined` is not "the user's locale", it is the MACHINE's — neither of the
|
|
96
|
+
repo's two locale channels. A German session read a dashboard KPI as `1,234.5`
|
|
97
|
+
next to a grid cell rendering the same number as `1.234,5`, and inverted
|
|
98
|
+
separators read as a different number, not as an unstyled one.
|
|
99
|
+
|
|
100
|
+
Both functions take the display locale as a new OPTIONAL LAST parameter, and
|
|
101
|
+
`DatasetWidget` threads `useDisplayLocale()` into every site it formats through:
|
|
102
|
+
the KPI, the grouped table's measure and dimension cells, and the cross-tab's
|
|
103
|
+
header labels and cells.
|
|
104
|
+
|
|
105
|
+
**English output does not move**, and that is the discriminator against the
|
|
106
|
+
sibling fix. These sites already went through `Intl` with default grouping, so
|
|
107
|
+
the only thing that changes is WHOSE locale is used:
|
|
108
|
+
|
|
109
|
+
| | before | after |
|
|
110
|
+
| ----------------- | ----------- | --------------------- |
|
|
111
|
+
| en, 1234.5 `0.0` | `1,234.5` | `1,234.5` (unchanged) |
|
|
112
|
+
| de, 1234.5 `0.0` | `1,234.5` | `1.234,5` |
|
|
113
|
+
| de, 1234.5 EUR | `€1,234.50` | `1.234,50 €` |
|
|
114
|
+
| de, 0.6083 `0.0%` | `60.8%` | `60,8%` |
|
|
115
|
+
|
|
116
|
+
Contrast objectui#4553, where `formatPercent` had never grouped at all and
|
|
117
|
+
moving en `1235%` → `1,235%` WAS the fix.
|
|
118
|
+
|
|
119
|
+
Omitting the new argument reproduces the previous output byte for byte, so
|
|
120
|
+
callers that do not thread a locale yet are unaffected.
|
|
121
|
+
|
|
122
|
+
Two behaviours are deliberately preserved rather than "improved" alongside the
|
|
123
|
+
locale fix, both measured:
|
|
124
|
+
|
|
125
|
+
- **Integers stay verbatim.** The integer branch renders no separator and no
|
|
126
|
+
decimal mark, so a locale has nothing to change there — and routing it through
|
|
127
|
+
`Intl` WOULD change it (a locale with its own numbering system re-digits it,
|
|
128
|
+
and `1e21` expands to 22 digits).
|
|
129
|
+
- **The percent sign stays a literal suffix.** `Intl`'s `style: 'percent'`
|
|
130
|
+
re-scales by 100, and that round trip loses precision at the top of the range
|
|
131
|
+
(en `100,000,000,000,000,000,000,000%` becomes
|
|
132
|
+
`99,999,999,999,999,990,000,000%`). The consequence — a German list cell
|
|
133
|
+
writing `1.234,5 %` with a no-break space where a dashboard measure writes
|
|
134
|
+
`1.234,5%` — is filed separately rather than smuggled in behind a locale fix.
|
|
135
|
+
|
|
136
|
+
`@object-ui/core` is `minor` because two of its ENTRY exports gained an optional
|
|
137
|
+
parameter (measured in the built `.d.ts`). `@object-ui/plugin-dashboard` is
|
|
138
|
+
`patch`: its published declarations are unchanged — `buildPivot`'s new optional
|
|
139
|
+
parameter is internal, as that function is not on the package's `exports`
|
|
140
|
+
surface.
|
|
141
|
+
|
|
142
|
+
- e901131: `DatasetResultField` is now `@objectstack/spec`'s `AnalyticsResult.fields[]` element itself, not a hand-written restatement of it
|
|
143
|
+
|
|
144
|
+
`packages/core/src/utils/dataset-format.ts` declared its own six-key interface for the analytics result column, under a doc comment describing the server's contract. The key set happened to match the spec today, so nothing was broken — but it was the last surviving member of the derive-don't-restate family (#3613 / #3753 on the parameter side, #3752 on the adapter return side), and it was the member with no compile-time tripwire: three surfaces (`plugin-dashboard`'s `DatasetWidget`, `plugin-report`'s `DatasetReportRenderer`, app-shell's `DatasetPreview`) consume this name AS the real column type, so the next spec column key would simply never appear here and no build would complain. It is now `AnalyticsResult['fields'][number]`, so it cannot lag the contract again.
|
|
145
|
+
|
|
146
|
+
**Consumer-visible type tightening (the reason this is a minor, not a patch).** The restatement had relaxed `type` to optional; the contract requires it. Anything that assigned a column literal without `type` — or a bare `{ name, label?, format? }` — to `DatasetResultField` will now fail to compile, and the fix is to supply the `type` the server always sends. Nothing in this repo needed changing: every value of this type originates in `ObjectStackAdapter.queryDataset`, which already declares the spec element, and no consumer reads `.type` at all, so the widening had bought no caller anything while advertising a `string | undefined` the wire never produces. Marked `minor` per the repo's bump policy, which reserves `major` for following `@objectstack/spec` across a major.
|
|
147
|
+
|
|
148
|
+
The exported name is unchanged and the `PercentScale` re-export from this module is untouched, so existing import paths keep working. `packages/core/tsconfig.typetests.json` (chained off the package's `type-check`) compiles the new parity test, so the pins are checked by CI rather than merely written down — including a negative pin that goes red if the hand-written interface is ever restored, and the `ChartResultField` superset relationship the module's comment claims.
|
|
149
|
+
|
|
150
|
+
- d9d3463: Retire four zero-consumer declared surfaces (dead-surface sweep batch 3, #4328). Each was
|
|
151
|
+
measured as declared-but-never-read at the branch point, and each is removed rather than
|
|
152
|
+
left as an authoring surface whose values nothing acts on.
|
|
153
|
+
|
|
154
|
+
Breaking for anyone who typed against the removed declarations, marked `minor` per this
|
|
155
|
+
repository's version-alignment convention (the major tracks `@objectstack`, never an
|
|
156
|
+
API-break count):
|
|
157
|
+
|
|
158
|
+
- `@object-ui/core` no longer exports `mergeViewsIntoObjects`. It was a second copy left
|
|
159
|
+
behind by the move of that step to the provider layer, and it had drifted: it ignored a
|
|
160
|
+
view container's default `list` and keyed views by the authored bare key instead of the
|
|
161
|
+
composer's `<object>.<key>` identity. The live implementation — `MetadataProvider`'s, in
|
|
162
|
+
`@object-ui/app-shell` — is unchanged and remains the only one. (#3775)
|
|
163
|
+
- `@object-ui/types`' `RoleDefinition` no longer declares `permissions`. A role's grants
|
|
164
|
+
live in `ObjectPermissionConfig.roles`, keyed by object; that is the only home any
|
|
165
|
+
consumer reads (`resolveRoles` walks `inherits` and matches on `name`). The removed
|
|
166
|
+
field was _required_, so five fixtures across three packages had been declaring an empty
|
|
167
|
+
array for a value nothing would ever look at. Role-attached grants are now a compile
|
|
168
|
+
error rather than silently ignored data. (#4288)
|
|
169
|
+
- `@object-ui/react`'s `RecordContextValue` no longer declares `loading` / `error`. Both
|
|
170
|
+
had zero producers and zero consumers — no host passed them, no `record:*` renderer read
|
|
171
|
+
them — and only the provider's memo dependency list still named them. Record-level
|
|
172
|
+
loading and error state stays where it is actually expressed: each renderer's own data
|
|
173
|
+
source. (#3773)
|
|
174
|
+
|
|
175
|
+
No behaviour change, no request-count change:
|
|
176
|
+
|
|
177
|
+
- `@object-ui/data-objectstack` drops five `metadataCache.invalidate('views:<object>')`
|
|
178
|
+
calls across `updateViewConfig` / `createView` / `updateView` / `deleteView`. No read
|
|
179
|
+
path has ever populated that key — `listViews` fetches directly, uncached — so all five
|
|
180
|
+
were permanent no-ops. The invalidations of the keys that do have readers
|
|
181
|
+
(`view:<object>:<viewId>` for `getView`, `view-overrides:<object>` for
|
|
182
|
+
`listViewOverrides`) are untouched and now pinned. (#3778)
|
|
183
|
+
|
|
184
|
+
- 38ab505: Retire the `global_nav` Studio designer surfaces, and track the `@objectstack` family at `17.0.0-rc.6` (objectstack#7100 / objectstack#6888).
|
|
185
|
+
|
|
186
|
+
## The retirement
|
|
187
|
+
|
|
188
|
+
`global_nav` was an `ACTION_LOCATIONS` member no running-app surface ever rendered. The console's ⌘K palette (`app-shell/src/chrome/CommandPalette.tsx`) builds its groups from nav items, objects, dashboards, pages, reports, recent items, record search and theme; it holds no reference to `global_nav`, to `actionRendersAt`, or to any action-metadata source. An action declaring `locations: ['global_nav']` therefore never reached a user.
|
|
189
|
+
|
|
190
|
+
The Studio designer previewed it anyway — a mock frame reading `⌘K · Command palette` with the author's button inside it. That is the sharp edge the maintainer's 2026-08-09 ruling on objectstack#6888 named: an authoring tool promising a surface the product does not have teaches authors, and every AI copying this corpus, to declare dead metadata. `@objectstack/spec` `17.0.0-rc.6` retired the member (7 members → 6) with a named rejection message; this release removes the designer surfaces that outlived it.
|
|
191
|
+
|
|
192
|
+
- `metadata-admin/previews/ActionPreview.tsx` — the mock command-palette placement frame is gone. The metadata strip above it still ECHOES whatever `locations` the draft declares, deliberately: reporting what a (possibly stale) draft says is honest, whereas the frame CLAIMED the platform renders it.
|
|
193
|
+
- `metadata-admin/inspectors/ActionDefaultInspector.tsx` — the `global_nav` entry is gone from `LOCATION_LABELS`. That map is typed `Record< ActionLocation, string >`, so the retirement reached it as a compile error rather than as a silently stale dropdown — the mechanism objectui#3017 installed, firing as designed.
|
|
194
|
+
- `metadata-admin/previews/block-config.ts` — the `record:quick_actions` location dropdown no longer offers it, and both locale tables drop the now-orphaned `…option.location.global_nav` key.
|
|
195
|
+
- `@object-ui/components`' `action:bar` doc comment is aligned. The component's published enum is `[...ACTION_LOCATIONS]`, so it followed the retirement on its own; only the prose was stale.
|
|
196
|
+
|
|
197
|
+
`@object-ui/core`'s `ActionEngine.getActionsForLocation` is **unchanged and still answers a literal string match**. Narrowing it to the six live members would put a second rejection point beside the schema's — the tolerant-consumer shape the strict-contract rule forbids, inverted. Enforcement stays where it belongs: the parameter type is now six-membered so no type-correct caller can spell the retired value, and `ActionLocationSchema` rejects it by name at authoring and publish time.
|
|
198
|
+
|
|
199
|
+
## The dependency move
|
|
200
|
+
|
|
201
|
+
All 37 `@objectstack/*` declarations across 30 `package.json` files move from `^17.0.0-rc.5` to `^17.0.0-rc.6`, and `pnpm-lock.yaml` resolves one copy of each family package at rc.6. The siblings move with `spec` because `client` / `formula` / `lint` pin it **exactly** — leaving them behind would keep two copies of the spec in the tree, the split brain objectui#3560 called out.
|
|
202
|
+
|
|
203
|
+
Bumping the pin and repairing the fallout cannot be split: at rc.5 the `Record< ActionLocation, string >` above is missing a key, at rc.6 it has an excess one.
|
|
204
|
+
|
|
205
|
+
## Breaking, in FROM → TO form
|
|
206
|
+
|
|
207
|
+
- **`@object-ui/types`' `Theme` now binds the spec's `Theme`, not `ThemeInput`.** rc.6 retired every `…Input` alias and moved the bare name onto the `z.input` side (`X` = `z.input`, `XParsed` = `z.infer`). The runtime shape and this package's exported name are unchanged — `Theme` was, and still is, the AUTHORING shape where `mode` is optional. Re-pointing at `ThemeParsed` would have been the silent swap.
|
|
208
|
+
- **`SpecReport` / `SpecReportChart` re-point to `ReportParsed` / `ReportChartParsed`, and `SpecReportInput` / `SpecReportChartInput` to `Report` / `ReportChart`.** Same rename, same rule: each local alias keeps the SIDE it had at rc.5.
|
|
209
|
+
- **`@object-ui/types` no longer re-exports `I18nObject`, `LocaleConfig`, `PluralRule`, `DateFormat` or `NumberFormat`** — all five were retired by rc.6. They were dead re-exports here: nothing in this repo imported them from `@object-ui/types` (`@object-ui/i18n`'s formatter vocabulary in `utils/spec-formatters.ts` is locally declared and never bound the spec symbols). `I18nLabel` survives and is unchanged as a name.
|
|
210
|
+
- **`I18nLabel` itself widened from `string` to `string | Record< string, string >`** — rc.6 folded the retired `I18nObject`'s per-locale map into it and ships `resolveI18nLabel(label, locale)` as the shared resolver. Every read in this repo that lands in a text slot now goes through that resolver, so an inline map renders its locale instead of `[object Object]`. Reads the compiler cannot see are audited separately in objectui#4163.
|
|
211
|
+
- **`@object-ui/types`' `GlobalFilterSchema` derives via `.safeExtend`, not `.extend`.** rc.6's `GlobalFilterSchema` carries a refinement and zod 4 refuses `.extend()` on a refined object outright, which threw at module load. `.safeExtend` is zod's prescribed replacement and KEEPS the refinement, so the spec's cross-field rule now also runs on this package's dialect — which is the intended behaviour, since the pinned divergences widen individual fields and were never meant to switch off a whole-object rule.
|
|
212
|
+
|
|
213
|
+
- 92250d6: One home for the number-display policy — and a percent stops meaning two different things between a list cell and a dashboard measure
|
|
214
|
+
|
|
215
|
+
`formatDisplayNumber`, `shouldGroupDisplayNumber` and `DisplayNumberFormatOptions` move from `@object-ui/i18n` into `@object-ui/core`. `@object-ui/i18n` re-exports all three under the same names, so every existing import path keeps working unchanged and both spellings resolve to the same function object; nothing published was removed.
|
|
216
|
+
|
|
217
|
+
The move is what fixes the bug. `@object-ui/core`'s `formatMeasure` needed exactly this policy and could not import it — `core` is the React-free engine and is a runtime dependency of React-free consumers (the `object-ui` VS Code extension, `@object-ui/data-objectstack`), while `i18n` depends on `i18next`/`react-i18next` and peer-depends on React. So `formatMeasure` carried a parallel `Intl` implementation, recorded at both ends as deliberate duplication, and the two drifted in the one place a hand-built string and `Intl` disagree. A German session read `1.234,5 %` from a list cell and `1.234,5%` from a dashboard measure showing the same number. The function is pure, so the boundary was never a property of the code — only of where the code sat; moving it down removes the obstacle instead of working around it. `core` imports nothing from `i18n`, so the new edge adds no cycle.
|
|
218
|
+
|
|
219
|
+
**Behaviour change — a measure's percent sign now follows the locale.** `formatMeasure` appended a literal `%` in every locale; it now renders the locale's own percent convention, the same one the list-cell `formatPercent` has used since the fix to its own machine-locale defect. Measured to change output in de, fr, es, ru, sv, cs, fi (a no-break space appears before the sign), tr (the sign moves to the FRONT: `%1.234,5`) and ar (its own percent sign plus U+061C). English, Japanese and Chinese are byte-identical — their convention is a bare trailing sign — which is why this was invisible in an English session.
|
|
220
|
+
|
|
221
|
+
**No numeral moves, in any locale, at any magnitude.** The obvious route to the locale's convention is `Intl`'s `style: 'percent'`, but that style expects a fraction, so a value already in percentage points would have to be divided by 100 for `Intl` to multiply it straight back — and that round trip is lossy. Measured, it moves 27,581 of 1,200,013 ordinary-magnitude en-US forms at rounding ties (`0.175` at two decimals becomes `0.17%` instead of `0.18%`), plus `MAX_SAFE_INTEGER` and everything from 1e23 up, where `100,000,000,000,000,000,000,000%` becomes `99,999,999,999,999,990,000,000%`. The percentage points are formatted directly instead, through a new `style: 'percentPoints'` on `DisplayNumberFormatOptions`; that route was measured to produce a byte-identical percent affix to `style: 'percent'` across all 171 locale tags tested while moving none of those 1,200,013 forms. Callers holding a fraction keep using `style: 'percent'`, whose behaviour is unchanged — naming the two cases apart is what stops the next caller from reaching for the lossy one.
|
|
222
|
+
|
|
223
|
+
`@object-ui/i18n`'s entry declaration is byte-identical, but the declaration it points at now lives in `@object-ui/core` and the package gains that dependency, so it takes the same minor bump rather than a patch.
|
|
224
|
+
|
|
225
|
+
- c1d939f: One `SchemaNode`, and one label vocabulary — the union wins, and labels resolve where the locale lives
|
|
226
|
+
|
|
227
|
+
Two packages published a type called `SchemaNode` and they were not the same type. `@object-ui/core` hand-declared `interface SchemaNode { type: string; … [key: string]: any }`; `@object-ui/types` exported `type SchemaNode = BaseSchema | string | number | boolean | null | undefined`, whose own doc comment names `'Plain string'` a valid node. Both were exported under one name from packages the same consumers import together, so which declaration a call site got depended on which package it happened to import from — #4548's canary measured 19 of 35 errors as exactly that collision. Core's declaration is now a re-export of types', so there is one declaration left to disagree with. Core's entry surface is unchanged: `dist/index.d.ts` is byte-identical across the change.
|
|
228
|
+
|
|
229
|
+
Reconciling it exposed a real defect rather than a mechanical narrowing, which is why the first attempt was withdrawn instead of forced. The spec bridges write `spec.label` — the spec's `I18nLabel`, an INLINE locale map like `{ en: 'Owner', 'zh-CN': '负责人' }` — into `node.label`, and `BaseSchema.label` declared `string`. Under core's old index signature that assignment was invisibly `any`; under one honest `SchemaNode` it is a type error. `BaseSchema.label` and `.description` therefore now accept `string | I18nLabel`, and the two bridge assignments compile with their expressions untouched.
|
|
230
|
+
|
|
231
|
+
Resolution happens at READ time, in the renderer, against the display locale — not at the bridge. Resolving at the bridge was measured unimplementable: it is a plain class method that cannot call a hook, `BridgeContext` declares no locale, and `updateContext()` has zero callers, so a bridge-resolved label would freeze one audience's language into the node tree with no re-translation channel. React's own invalidation re-translates for free at the read site.
|
|
232
|
+
|
|
233
|
+
The widening turned every blind `schema.label`-as-string read into a named compiler error, and that inventory is the audit: it named four sites repo-wide, all one class — the label reaching a React child position, where a map does not render as `[object Object]` but THROWS `Objects are not valid as a React child`, failing the whole subtree. Three are `@object-ui/components` renderers (`filter-builder`, `sidebar-group`, `dropdown-menu`), which now resolve with the spec's own `resolveI18nLabel` against `useDisplayLocale()`. The fourth is `plugin-dashboard`'s `DashboardGridLayout` heading, which resolves with `pickLocalized` against the active UI language — matching the widget-title resolution already in that same component rather than putting two resolvers and two disagreeing locale channels in one render; the two resolvers are limb-for-limb twins with a parity test pinning them.
|
|
234
|
+
|
|
235
|
+
One interface now carries both label vocabularies two properties apart — `label`/`description` are the spec's INLINE map, `ariaLabel` is the KEYED bundle reference — and each accepts the other's shape vacuously. That confusability is objectui#4167's known hazard, inherent to the spec's `I18nLabel` design; both shapes are named with cross-referenced doc comments stating which resolver owns which slot, and a pin asserts the two unions do not collapse into each other.
|
|
236
|
+
|
|
237
|
+
Finally, the spec bridges declare their return type as `BaseSchema` instead of the union. Both bridges end in a single `return node` on an object literal, so the union described nothing real while forcing a narrowing at every read — 272 mechanical errors across five suites in the first round. That change is a type annotation only; the emitted JavaScript is byte-identical.
|
|
238
|
+
|
|
239
|
+
- 2459a3e: Retire `ActionEngine`'s event-mapping API (objectui#3368). `ActionEngine.addMapping()`,
|
|
240
|
+
`ActionEngine.dispatch()`, the private `mappings` registry behind them, and the exported
|
|
241
|
+
`ActionMapping` interface are removed under enforce-or-remove: all four were public surface
|
|
242
|
+
of `@object-ui/core` with zero production callers. Nothing in the repo ever registered a
|
|
243
|
+
mapping, so `dispatch()` had no reachable caller either, and every call site was in the
|
|
244
|
+
engine's own test file.
|
|
245
|
+
|
|
246
|
+
Breaking for anyone who typed against or called the removed declarations, marked `minor`
|
|
247
|
+
per this repository's version-alignment convention (the major tracks `@objectstack`, never
|
|
248
|
+
an API-break count). Actions are still entered by name (`executeAction`), by location
|
|
249
|
+
(`getActionsForLocation`), by shortcut (`handleShortcut`) and in bulk (`executeBulk`) —
|
|
250
|
+
only the event-keyed entry point is gone, and no runtime behaviour changes because no
|
|
251
|
+
runtime path reached it.
|
|
252
|
+
|
|
253
|
+
The three ways the retired condition gate had drifted from the `visible` contract that
|
|
254
|
+
`getActionsForLocation` implements die with the path rather than being fixed on it: it
|
|
255
|
+
entered on a raw truthy check (`condition: false` dispatched anyway), typed `condition` as
|
|
256
|
+
`string` only (a `{ dialect: 'cel', source }` envelope could not reach the canonical
|
|
257
|
+
`@objectstack/formula` engine), and evaluated without `throwOnError` (a throwing predicate
|
|
258
|
+
failed OPEN, the opposite of `visible`'s fail-closed posture). Aligning the contract of an
|
|
259
|
+
API nobody calls would only have widened behaviour nobody uses.
|
|
260
|
+
|
|
261
|
+
- fe52a04: `rowHeightToDensityMode` answers only for the five spec row heights — the coerce-to-`comfortable` fallback is gone
|
|
262
|
+
|
|
263
|
+
Two surfaces narrow a list view's `rowHeight` onto the renderer's three-step
|
|
264
|
+
density vocabulary, and since objectui#4352 they answered differently for the
|
|
265
|
+
same off-spec input: `@object-ui/react`'s spec bridge declined to answer, while
|
|
266
|
+
`@object-ui/core`'s `rowHeightToDensityMode` rehabilitated anything unknown into
|
|
267
|
+
`comfortable`. One metadata-driven system, two answers for one input
|
|
268
|
+
(objectui#4440).
|
|
269
|
+
|
|
270
|
+
The strict answer wins, per AGENTS.md #0.1: a renderer-side rehabilitation of
|
|
271
|
+
off-spec metadata is a second de-facto contract, and one strict contract beats N
|
|
272
|
+
dialects — a bad `rowHeight` gets fixed at the producer, where the schema already
|
|
273
|
+
rejects it. The five mappings themselves are untouched (`compact`/`short` →
|
|
274
|
+
`compact`, `medium` → `comfortable`, `tall`/`extra_tall` → `spacious`), and the
|
|
275
|
+
table keeps its `Record< RowHeight, … >` typing, so a row height added upstream
|
|
276
|
+
still fails the build here.
|
|
277
|
+
|
|
278
|
+
**Breaking semantics, deliberately graded `minor`** (this repo never publishes
|
|
279
|
+
`major` — its major tracks `@objectstack`). Two things change:
|
|
280
|
+
|
|
281
|
+
- **Published type.** `rowHeightToDensityMode` is exported from
|
|
282
|
+
`@object-ui/core`, and its return widens from `DensityMode` to
|
|
283
|
+
`DensityMode | undefined`. A host assigning the result straight into a
|
|
284
|
+
`DensityMode` now has to say what an off-spec row height should mean to it.
|
|
285
|
+
- **Rendered output, for input the spec already rejects.** `ListView` — the one
|
|
286
|
+
in-repo caller — used to render an off-spec `rowHeight` one step looser than an
|
|
287
|
+
ABSENT one (`comfortable`, 40px rows, vs `compact`, 32px). It now renders it
|
|
288
|
+
exactly like an absent one, `compact`, which is also `ObjectGrid`'s own default.
|
|
289
|
+
A sweep of this repo, the `objectstack` example apps and one downstream app
|
|
290
|
+
found zero authored off-spec values, and the legacy `densityMode` alias cannot
|
|
291
|
+
produce one (`DENSITY_MODE_TO_ROW_HEIGHT` is typed
|
|
292
|
+
`Record< DensityMode, RowHeight >`).
|
|
293
|
+
|
|
294
|
+
Also closed while retiring the branch: the lookup guarded membership with `in`,
|
|
295
|
+
which walks the prototype chain, so `rowHeight: 'toString'` returned
|
|
296
|
+
`Object.prototype.toString` — a function — from something typed `DensityMode`. It
|
|
297
|
+
is an own-property check now.
|
|
298
|
+
|
|
299
|
+
- bb68488: Stop declaring 14 symbols under names `@objectstack/spec` owns at `17.0.0-rc.6`
|
|
300
|
+
(objectui#4167, objectstack#4115).
|
|
301
|
+
|
|
302
|
+
The rc.6 bump published nine names this repo already declared locally, on top of
|
|
303
|
+
four that predate it — `check:spec-symbols` reported all thirteen at once, and a
|
|
304
|
+
fourteenth (`GlobalFilterSchema`) appeared during the bump itself. Each was
|
|
305
|
+
triaged on its own rather than blanket-renamed, because the right answer differs
|
|
306
|
+
per symbol: five bind to the spec, three are renamed because the spec's
|
|
307
|
+
same-named export means something else, five arrive by derivation, and one is a
|
|
308
|
+
declared dialect with a written reason.
|
|
309
|
+
|
|
310
|
+
**Breaking for importers of `@object-ui/react`, `@object-ui/app-shell` and
|
|
311
|
+
`@object-ui/types`** — three exported names changed, because the spec exports the
|
|
312
|
+
same name for a _different_ thing:
|
|
313
|
+
|
|
314
|
+
| package | was | now | what the spec's same-named export actually is |
|
|
315
|
+
| :-------------------- | :----------------- | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
316
|
+
| `react` / `app-shell` | `MetadataState` | `MetadataCacheState` | a metadata item's LIFECYCLE state — `'draft' \| 'active' \| 'deprecated' \| 'archived'` (`MetadataStateSchema`, `@objectstack/spec/system`) |
|
|
317
|
+
| `react` / `app-shell` | `resolveI18nLabel` | `resolveKeyedI18nLabel` | a resolver for the INLINE per-locale map (`{ en: 'Owner', 'zh-CN': '负责人' }`) against a BCP-47 locale |
|
|
318
|
+
| `types` | `DateRangePreset` | `FilterBuilderDateRangePreset` | the thirteen HISTORICAL dashboard filter-bar presets; this one is the filter-builder set, which adds eight FUTURE windows the dashboard schema rejects |
|
|
319
|
+
|
|
320
|
+
`resolveI18nLabel` is the one where the collision had already started costing
|
|
321
|
+
something. rc.6 widened `I18nLabel` from `string` to
|
|
322
|
+
`string | Record< string, string >`, so the same authored value now reaches
|
|
323
|
+
either resolver — and each answers wrongly, silently, for the other's input: the
|
|
324
|
+
keyed one returns `undefined` for `{ en: 'Owner' }` (no `key`, no
|
|
325
|
+
`defaultValue`), and the spec's reads `key` / `defaultValue` / `params` as locale
|
|
326
|
+
tags. The rc.6 bump PR met this and aliased the spec's import as
|
|
327
|
+
`resolveInlineI18nLabel` in five files, with hand-written comments at two of
|
|
328
|
+
them. That is a review convention, which is what objectstack#4115 exists to
|
|
329
|
+
replace with a rule — so `Keyed` is now the counterpart of that `Inline`, and the
|
|
330
|
+
name says which vocabulary it resolves at every call site.
|
|
331
|
+
|
|
332
|
+
**Eleven keep their names and are now imported or derived from the spec** instead
|
|
333
|
+
of re-declared: `DATE_RANGE_PRESETS`, `NavigationMode`, `AddressValue`,
|
|
334
|
+
`BreakpointColumnMap`, `BreakpointOrderMap`, `KanbanConfig`, `CalendarConfig`,
|
|
335
|
+
`GanttConfig`, plus the three renamed above at their new names.
|
|
336
|
+
|
|
337
|
+
**Four of the copies were losing information, not just duplicating it.**
|
|
338
|
+
|
|
339
|
+
- **`GanttConfig` declared six keys and called itself canonical; rc.6's
|
|
340
|
+
`GanttConfigSchema` declares seventeen.** The eleven it never mentioned —
|
|
341
|
+
`parentField`, `typeField`, `baselineStartField`, `baselineEndField`,
|
|
342
|
+
`groupByField`, `resourceView`, `assigneeField`, `effortField`, `capacity`,
|
|
343
|
+
`quickFilters`, `autoZoomToFilter` — are all read by
|
|
344
|
+
`plugin-gantt/src/ObjectGantt.tsx`, through a local `GanttConfigEx`
|
|
345
|
+
intersection that existed only because this type did not carry them. It now
|
|
346
|
+
derives from the spec, with `timeSegments` (shift segmentation) as the one
|
|
347
|
+
genuinely local extension; the schema is `$loose` upstream, so that key is
|
|
348
|
+
legal metadata rather than a second dialect.
|
|
349
|
+
- **`GanttConfig.tooltipFields` carried the comment "not part of the upstream
|
|
350
|
+
GanttConfigSchema".** It is, as of rc.6, so the key now arrives from the spec.
|
|
351
|
+
- **`AddressValue` declared five of the spec's seven parts** — `countryCode` and
|
|
352
|
+
`formatted` were missing, under a comment already claiming to be "the part
|
|
353
|
+
names of `AddressSchema`". The widget still renders five inputs; binding the
|
|
354
|
+
type stops it from asserting the platform cannot store the other two, and makes
|
|
355
|
+
the `{ ...address }` write-through say so.
|
|
356
|
+
- **`DATE_RANGE_PRESETS` was `Object.keys(PRESET_RANGES)`,** a third copy of a
|
|
357
|
+
vocabulary the spec extracted in objectstack#4614 precisely to collapse — its
|
|
358
|
+
own doc comment names this module as one of the three. It is now the spec's
|
|
359
|
+
array by reference, and the local date-macro bounds table is pinned complete
|
|
360
|
+
against it with `satisfies`, so a preset the schema gains without bounds here
|
|
361
|
+
is a compile error rather than a filter that validates clean and then selects
|
|
362
|
+
nothing.
|
|
363
|
+
|
|
364
|
+
`NavigationMode` was one hop from the spec already (`NavigationConfig['mode']`);
|
|
365
|
+
it is bound directly, with a both-directions type pin that it stays the same type
|
|
366
|
+
as the config's own `mode`. `KanbanConfig` / `CalendarConfig` /
|
|
367
|
+
`BreakpointColumnMap` / `BreakpointOrderMap` were exact hand copies of `$strict`
|
|
368
|
+
schemas and are now re-exports — "still exact" is the argument for binding them,
|
|
369
|
+
since a copy with nothing to protect can only drift.
|
|
370
|
+
|
|
371
|
+
`GlobalFilterSchema` is the one ALLOW entry. It is the same spread-composition
|
|
372
|
+
dialect as `SelectOptionSchema` next to it, and it collided only because rc.6's
|
|
373
|
+
new refinement forced `.extend()` to be respelled as a `.shape` spread — which
|
|
374
|
+
moved a derivation the guard could see into an object literal it deliberately
|
|
375
|
+
does not descend into. The dialect is unchanged and its three divergences are
|
|
376
|
+
pinned; which side moves on the refinement itself is objectui#4165.
|
|
377
|
+
|
|
378
|
+
`@objectstack/spec` moves from `devDependencies` to `dependencies` in
|
|
379
|
+
`@object-ui/layout`: its public type surface now references the spec.
|
|
380
|
+
|
|
381
|
+
### Patch Changes
|
|
382
|
+
|
|
383
|
+
- ee26e65: Analytics: the dimension label net's fetch-and-memo glue is written once, not once per surface
|
|
384
|
+
|
|
385
|
+
PR #4388 (objectui#4330) put the same React glue on two surfaces — the dashboard's `DatasetWidget` and plugin-report's dataset block. The resolution RULES were never duplicated (both call the same `@object-ui/core` helpers), but the wiring around them was: read the object schema through the host's authenticated `apiFetch`, keep the fetched metadata locale-free in state, derive the label maps in a render memo. Two copies meant two statements of the same two bug fixes, which is a drift surface rather than a defect — nothing a user could hit today, filed as objectui#4389 so it was retired deliberately.
|
|
386
|
+
|
|
387
|
+
It is now split along the layer that can actually hold each half. `@object-ui/core` gains the React-free parts — `loadDimensionFieldMeta` (the base-object read composed with the dimension walk), `deriveDimensionLabelMaps` (the locale-applying derivation) and `dimensionOptionTranslator` (binding the bundle resolver to the object that OWNS a terminal field, which for a dotted path is the relationship target). `@object-ui/react` gains `useDatasetDimensionLabels` / `useDatasetDimensionMeta`, the React wiring that cannot live in core, beside the `useViewData` / `useElementDataSource` / `useDiscovery` hooks that already read `SchemaRendererContext` the same way. Both plugins consume it; the dashboard keeps its chart-only per-category colour and category-order derivation layered locally, since a table renders no palette.
|
|
388
|
+
|
|
389
|
+
The card originally proposed `@object-ui/core` as the whole glue's home. That home was disproven by measurement and retired in the card's PM RULING #2: `SchemaRendererContext` is defined in `@object-ui/react`, which depends on core, so core importing it back is a cycle — and core is React-free by declaration, by content, and by the topology in AGENTS.md. objectui#3367 had already ruled this direction for the same family (core-canonical logic, react re-exports).
|
|
390
|
+
|
|
391
|
+
Behaviour is unchanged by construction: same read count, same best-effort fallback, same memoization boundary. The two bug fixes are now stated once and pinned at the shared hook — the read rides the host's authenticated `apiFetch` (objectui#4121, pinned by asserting that a new channel re-issues the read, i.e. that it really is in the effect's deps), and the fetched metadata stays locale-free (objectui#4030 / PR #4324, pinned by switching language at runtime and asserting the labels flip with no second metadata read). All 39 assertions PR #4388 landed across both surfaces pass unchanged, and their files are byte-identical to before.
|
|
392
|
+
|
|
393
|
+
- 5900ac5: Analytics surfaces now run resolved select-option labels through the locale bundle — the chart legend and the related list on one page stop disagreeing
|
|
394
|
+
|
|
395
|
+
A dashboard widget grouped by a `select` field rendered the option's authored English label while the related list beside it rendered the translation. The decisive evidence in objectui#4030 is the stored value `orion`: the chart read `Orion Engineered Carbons`, a string with no resemblance to the value and matching the object's `label` byte for byte. So the analytics path had already RESOLVED the option label — it simply never ran the result through the i18n bundle before display. (`domestic → Domestic` differs from its value by case alone, which is why the first diagnosis, "the report groups by stored value", was wrong.)
|
|
396
|
+
|
|
397
|
+
There is exactly one resolution channel and this change reuses it rather than adding a chart-side dialect: `fieldOptionLabel` from `useObjectLabel`, i.e. `{ns}.fieldOptions.<object>.<field>.<value>` — the convention `@objectstack/spec` names objectui as the reader of, and the one list, form, kanban and record-picker surfaces already translate select options through. The bundle is applied ONCE, at the output of the label net that landed in objectui#4053/#4263, on the shared option list every consumer reads: chart axis and legend, the table/pivot cells of a dotted dimension, that table's CSV export, per-category colours and the declared category order. `@object-ui/core` gains `localizeFieldOptions` (the pure mirror of `translateOptions`), an optional translator on `buildDimensionLabelMap`, and `resolveDimensionFieldMeta` — the same single relationship walk `resolveDimensionFieldOptions` performs, now keeping the object that OWNS the terminal field, because for `crm_account.industry` the bundle key is `crm_account`, not the dataset's base object.
|
|
398
|
+
|
|
399
|
+
Two properties the fix is shaped around. The rows reach this net keyed either way — by stored value when the server did not resolve the dimension, by the English label when it did (ADR-0021) — and the reported screen is the second case, so the map answers to both keys and lands on the same translated display. And identity is untouched: `relabelDimensions` still rewrites display only, so a drilled chart segment clicked as `欧励隆` filters by `orion`, bucket ids and pivot totals keep their raw keys, and an option with no bundle entry (or an `en` console) renders exactly the authored label it renders today.
|
|
400
|
+
|
|
401
|
+
The per-locale work moved from the metadata fetch into the render, so switching language now re-labels in place instead of waiting for a refetch.
|
|
402
|
+
|
|
403
|
+
Not covered, and unchanged here: a LOCAL select dimension on a table/pivot, whose label the server resolves and whose client-side net is deliberately off (objectui#4263), and a dashboard global filter's own field label, which has no object name in its metadata to key a bundle lookup with — tracked on objectui#4030.
|
|
404
|
+
|
|
405
|
+
- aca27fa: The multi-dimension pivot branch buckets a null first-dimension value instead of dropping its bar (objectui#4497)
|
|
406
|
+
|
|
407
|
+
`buildChartSeries`' pivot branch (2+ dimensions, single measure) bucketed rows by `String(xRaw ?? '')` but wrote the RAW value into the emitted row, so a null first-dimension value produced `{status: null, Low: 3}` and reached recharts with a null category — which draws no mark. Measured at the DOM: a two-group pivot drew ONE bar, and an all-null pivot drew axes and gridlines with zero bar rectangles and no empty state. That is the same mechanism objectui#4466 fixed one branch below, on the branch that card deliberately left pinned as-is until the pivot's own bucketing had been measured.
|
|
408
|
+
|
|
409
|
+
The pivot now maps a null/undefined first-dimension VALUE to the same bucket label the single-dimension branch uses — `ChartSeriesOptions.nullCategoryLabel`, defaulting to `NULL_CATEGORY_LABEL`. One doctrine, one predicate, two call sites; no new export, and every existing call site compiles and behaves identically.
|
|
410
|
+
|
|
411
|
+
The bucket KEY is untouched, which is what keeps this a display fix: `String(xRaw ?? '')` still decides which rows share a bar, so every existing grouping is byte-identical and only the label the bucket carries changes. Rows that lack the category key entirely are still not bucketed — that shape is a dimension grouped by but never projected (framework#4033), a different defect with a different answer.
|
|
412
|
+
|
|
413
|
+
Drill-through needed no change, which was measured rather than assumed: the pivot's emitted rows are AGGREGATED, so they are not index-aligned with `drillRawRows` and the one production caller (`DatasetWidget.handleChartDrill`) already drills by SEARCHING the raw rows through `findChartSeriesRow`. Those raw rows still carry their null, and objectui#4466's label-matching covers the multi-dimension arm as well as the single-dimension one, so the newly-visible bar resolves to the right record. Pinned at both levels so a regression in either half surfaces as the dead click it would be.
|
|
414
|
+
|
|
415
|
+
- 613b167: A dataset dimension on a dotted relationship path now renders its option labels instead of the raw stored enum
|
|
416
|
+
|
|
417
|
+
A `DatasetDimension` whose `field` is a relationship path (`crm_account.industry`) got no select-option resolution at all: the chart plotted `education`, `finance`, `manufacturing` — the database column, unresolved — while the **same underlying field** reached as a **local** dimension rendered `Education`, `Finance`, `Manufacturing` beside it on the same dashboard. Nothing errored, so the widget just quietly showed database enum values to end users; on a non-English deployment those are words that appear nowhere else in the UI, since every form and list shows the translated label.
|
|
418
|
+
|
|
419
|
+
The label lookup read options as `baseObject.fields[<path>]`, which only ever matches the local spelling. For a dotted path the options live on the **related** object, so the lookup missed and the renderer fell through to the stored value.
|
|
420
|
+
|
|
421
|
+
The object-resolution step of that one lookup now walks the path: each segment before the last must be a declared relationship (`lookup` / `master_detail`, target read from `reference` / `reference_to` / `referenceTo` / `reference_to_object`), and the terminal field's options are read off the object that actually owns it. This is the same lookup for both spellings rather than a dotted-path variant beside it — a single-segment path never enters the walk and resolves exactly as before, so the local and joined paths cannot drift apart. Multi-hop paths (`crm_account.owner.department`) resolve too, which is the shape the dataset designer already emits.
|
|
422
|
+
|
|
423
|
+
Hops ride the caller's existing `GET /meta/object/:name` channel — the same authenticated read that fetched the base object — so no new fetch layer is introduced, and objects are fetched once per resolution even when several dimensions share a prefix. Every failure stays best-effort: a segment that is not a relationship, a target that cannot be loaded, or a terminal field with no options yields no mapping and the raw value survives, exactly as it does today.
|
|
424
|
+
|
|
425
|
+
Applies to both surfaces that carried this lookup: dashboard dataset widgets (`DatasetWidget`) and the chart view's dataset path (`ObjectChart`).
|
|
426
|
+
|
|
427
|
+
Scope: this ends at "the label is in hand". Whether that label then passes through the i18n bundle is a separate gap tracked upstream as objectstack#5076.
|
|
428
|
+
|
|
429
|
+
- abb0f81: A dashboard date filter's default has one spelling again — the bare preset name — and the `{ preset }` object becomes a documented legacy alias with a retirement window
|
|
430
|
+
|
|
431
|
+
`@objectstack/spec` 17.0.0-rc.6 added a cross-field refinement to `GlobalFilterSchema` holding a `type: 'date'` filter's `defaultValue` to three spellings: a preset NAME (`last_7_days`), an ISO date (`2026-01-15`), or a date-macro token (`{today}`). objectui's derived schema had widened `defaultValue` to `z.any()` and did not carry the refinement, so it accepted `{ preset: 'last_7_days' }` — metadata the platform refuses. That is the tolerant-consumer shape where the designer goes green and the save fails server-side, and it is now closed: the refinement is adopted, the widening is retired, and the object form is refused with the spec's own message.
|
|
432
|
+
|
|
433
|
+
Per the maintainer ruling on objectui#4165, the spec stays strict and the bare preset name is the single canonical spelling. `{ preset }` is handled as an ADR-0089 legacy alias rather than by a permanently tolerant schema: `liftLegacyGlobalFilterDefault` / `liftLegacyDashboardFilterDefaults` (new exports on `@object-ui/types`) convert it to the bare name, `@object-ui/core`'s `resolveDashboardFilterDefs` applies the lift when it reads a stored dashboard, and the console's dashboard designer applies it as the document enters the editable draft so the next save persists the canonical spelling. The retirement window is recorded at the read site: the alias may be removed in `@object-ui/types` 18.0.0, and every lift warns on the console so a surviving legacy document is visible rather than silently tolerated.
|
|
434
|
+
|
|
435
|
+
No stored dashboard has to change for this release. The lift means a document carrying the object form keeps loading and rendering exactly as before — measured, not assumed: a legacy declaration already resolved correctly, because `{ preset }` also happens to be the runtime value shape objectui's own date filters use, and that coincidence is why the object form went unnoticed for so long. What changes is that the declaration is now canonicalized on read and rewritten on save, so the two spellings converge instead of accreting.
|
|
436
|
+
|
|
437
|
+
The other two divergences in this schema — the bare-string `options` shorthand and the optional `optionsFrom.labelField` — are unaffected. Carrying the spec's refinement while keeping them needed a new composition: a refined object schema in zod 4 rejects `.extend()` and `.omit()` outright and types every `.safeExtend()` override as `never`, so objectui's schema now spreads the spec's shape and re-attaches the spec's object-level rules by delegating to the spec schema itself. Nothing restates the spec's grammar, and a refinement the spec adds later flows in with no change here.
|
|
438
|
+
|
|
439
|
+
- 7e4f0e5: fix(dashboard,i18n): KPI cards and dashboard filters resolve authored labels instead of dropping them (#4032)
|
|
440
|
+
|
|
441
|
+
A `type: 'metric'` dashboard widget rendered raw English while every other widget
|
|
442
|
+
type on the same dashboard rendered the translation, and dashboard filter chips
|
|
443
|
+
rendered `[object Object]` or the raw stored value. Both come from the same
|
|
444
|
+
cause: authored labels reaching a render site that could not read the
|
|
445
|
+
vocabulary `@objectstack/spec` actually admits.
|
|
446
|
+
|
|
447
|
+
- **KPI cards rejoin the widget translation channel.** The self-contained
|
|
448
|
+
`metric` branch built its own label from the raw `widget.title`, so the
|
|
449
|
+
`{ns}.dashboards.{dash}.widgets.{id}.title` value the renderer had already
|
|
450
|
+
resolved was computed and thrown away. It now reads that channel like every
|
|
451
|
+
other widget header.
|
|
452
|
+
- **The three private `resolveLabel` copies** (`DashboardRenderer`,
|
|
453
|
+
`MetricWidget`, `MetricCard`) are gone. Each read the retired
|
|
454
|
+
`{ key, defaultValue }` key-reference form and ended `defaultValue || key`, so
|
|
455
|
+
handed the inline per-locale map the spec admits today they returned nothing —
|
|
456
|
+
a KPI card with a map title rendered the literal string `metric`. All three
|
|
457
|
+
now use `pickLocalized`, the resolver already used for this vocabulary
|
|
458
|
+
elsewhere in the package.
|
|
459
|
+
- **Dashboard filter labels and static option labels resolve per locale.**
|
|
460
|
+
`DashboardFilterDef.label` widens to `string | I18nLabel`, the filter bar
|
|
461
|
+
resolves before rendering (fixing `[object Object]: All` in the trigger, and
|
|
462
|
+
in `aria-label` / `placeholder`), and the `def.label || def.name` gate now
|
|
463
|
+
tests the RESOLVED string — an object is always truthy, so it never reached
|
|
464
|
+
the fallback before.
|
|
465
|
+
- **Option labels are no longer discarded.** `normalizeFilterOptions` coerced a
|
|
466
|
+
map label to the raw stored value in every locale, English included, so
|
|
467
|
+
`{ value: 'domestic', label: { en: 'Domestic', … } }` displayed as `domestic`.
|
|
468
|
+
The pair shape is still normalized; the label vocabulary is preserved for the
|
|
469
|
+
render side to resolve.
|
|
470
|
+
- **`DashboardComponentSchema.globalFilters` is bound to the spec's
|
|
471
|
+
`GlobalFilter`** instead of restated by hand. The restatement was both too
|
|
472
|
+
narrow (`label?: string`, which is what made these read sites invisible to
|
|
473
|
+
`tsc`) and too wide (it declared a bare-string option shorthand the spec
|
|
474
|
+
rejects at publish).
|
|
475
|
+
|
|
476
|
+
Plain-string labels are unaffected and render byte-identically.
|
|
477
|
+
|
|
478
|
+
- 49ae9f4: Pivot buckets encode an empty dimension value as JSON `null`, so it no longer collides with a row whose value is literally the placeholder character
|
|
479
|
+
|
|
480
|
+
objectstack#5473 / objectstack#5665 replaced the pivot's delimiter-joined ids
|
|
481
|
+
with `JSON.stringify`, because every delimiter that had been tried — an empty
|
|
482
|
+
string, a plain space, a control character — assumed the data would not contain
|
|
483
|
+
it, and each assumption failed on ordinary data. This closes the last place the
|
|
484
|
+
same assumption survived: the ids were JSON, but the VALUES fed into them were
|
|
485
|
+
spelled `String(row[d] ?? '∅')`, so an absent dimension value became the
|
|
486
|
+
ordinary string `"∅"` and shared a bucket with a row whose value literally is
|
|
487
|
+
that character (U+2205). One bucket, later row overwriting the earlier one — the
|
|
488
|
+
cell showed a different row's measure, the overwritten row was unreachable, and
|
|
489
|
+
drill-through followed the same wrong index into the wrong records, all without
|
|
490
|
+
an error. The trigger requires that character to appear as a dimension value, so
|
|
491
|
+
this is the assumption being removed rather than a defect users hit today.
|
|
492
|
+
|
|
493
|
+
An empty value now encodes as JSON `null`, which `JSON.stringify` renders as a
|
|
494
|
+
bare `null` that no string can spell. The normalization lives in
|
|
495
|
+
`@object-ui/core` as `pivotDimensionValue` (absent ⇒ `null`, everything else ⇒
|
|
496
|
+
its string form) rather than at each call site, because a placeholder spelled by
|
|
497
|
+
a caller is a placeholder that can collide again — which is exactly how this one
|
|
498
|
+
survived the previous fix. `pivotBucketId` accepts `Array<string | null>`
|
|
499
|
+
accordingly; that is a widening, so existing callers passing `string[]` are
|
|
500
|
+
unaffected.
|
|
501
|
+
|
|
502
|
+
Both renderers' bucket keys move together, which the fix requires: a bucket id
|
|
503
|
+
and the subtotal map keyed by it are built from the same expression, so changing
|
|
504
|
+
one alone would split the headers while the subtotal map still merged, landing
|
|
505
|
+
every column subtotal under the wrong header. In `plugin-dashboard`'s
|
|
506
|
+
`DatasetWidget` that is the row bucket id, the column bucket id, the cell key,
|
|
507
|
+
and both the `rowTotalById` and `colTotalById` lookups; in `plugin-report`'s
|
|
508
|
+
`DatasetReportRenderer` the single `bucketId` helper already feeds all five.
|
|
509
|
+
|
|
510
|
+
The dashboard's column bucket id also stops being a bare string and becomes a
|
|
511
|
+
one-element tuple through the same shared encoder. It was the one id in the
|
|
512
|
+
family still built by hand, on the reasoning that a single value needs no
|
|
513
|
+
boundary — true of the boundary, false of everything else the encoder does, and
|
|
514
|
+
it is why the across axis kept carrying this collision after the row ids were
|
|
515
|
+
fixed.
|
|
516
|
+
|
|
517
|
+
No display change: these placeholders only ever entered ids, never labels. An
|
|
518
|
+
unset dimension still renders through `formatDimensionValue` exactly as before,
|
|
519
|
+
and data containing neither an absent value nor that character buckets
|
|
520
|
+
identically — the ids are opaque lookup keys, never parsed back into a value,
|
|
521
|
+
never shown, never persisted.
|
|
522
|
+
|
|
523
|
+
- d6aa172: Retire `params.newTab` on a url action — `openIn: 'new-tab'` is the sanctioned spelling
|
|
524
|
+
|
|
525
|
+
`ActionRunner`'s navigator read a legacy `params.newTab` escape hatch below `openIn` and above the external-URL heuristic. That read is removed, executing the objectstack#6828 maintainer ruling of 2026-08-10, whose contract half shipped in objectstack PR #7375: the url-side readings of an object-form `params` are retired, not renamed.
|
|
526
|
+
|
|
527
|
+
Nothing that ever validated can regress. `params` is declared as `z.array(ActionParamSchema)`, so an object-form `params` has always failed the props parse — the fallback could only fire on a stack the spec refuses. The removal also closes a collision hazard: a params dialog declaring a field named `newTab` had the user's own collected input silently steering navigation.
|
|
528
|
+
|
|
529
|
+
`openIn: 'self' | 'new-tab'`, the legacy `navigate.newTab` modifier on the `navigation` shape, and the external/relative default are all unchanged.
|
|
530
|
+
|
|
531
|
+
- 9461dd3: Form actions no longer carry a record id across an object boundary (#4292).
|
|
532
|
+
|
|
533
|
+
`ActionRunner.executeForm` forwarded `/forms/:name?recordId=<id>` unconditionally,
|
|
534
|
+
and that URL says nothing about which object the id belongs to — so the form route
|
|
535
|
+
resolved it against the FormView's own target object. When an action fired from a
|
|
536
|
+
record of a DIFFERENT object and ids collide across objects (per-table integer
|
|
537
|
+
keys), the form silently prefilled and, since the route learned to honour the param,
|
|
538
|
+
`PATCH`ed a same-id record of the wrong object.
|
|
539
|
+
|
|
540
|
+
- **Producer**: the id is forwarded only when the firing context record's object
|
|
541
|
+
(`context.objectName`) matches the target view's object; on a mismatch no id is
|
|
542
|
+
forwarded, preserving create semantics. When it IS forwarded, the object travels
|
|
543
|
+
with it as `?recordObject=`.
|
|
544
|
+
- **Consumer**: `/forms/:name` refuses — no record read, no write — when
|
|
545
|
+
`recordObject` disagrees with the FormView's object. A URL without the param
|
|
546
|
+
behaves exactly as before, so existing deep links are unaffected.
|
|
547
|
+
|
|
548
|
+
- Updated dependencies [f650253]
|
|
549
|
+
- Updated dependencies [3d9769a]
|
|
550
|
+
- Updated dependencies [92876f0]
|
|
551
|
+
- Updated dependencies [d9d3463]
|
|
552
|
+
- Updated dependencies [2a40f69]
|
|
553
|
+
- Updated dependencies [bec3e14]
|
|
554
|
+
- Updated dependencies [1f9b905]
|
|
555
|
+
- Updated dependencies [abb0f81]
|
|
556
|
+
- Updated dependencies [38ab505]
|
|
557
|
+
- Updated dependencies [7e4f0e5]
|
|
558
|
+
- Updated dependencies [c1d939f]
|
|
559
|
+
- Updated dependencies [bb68488]
|
|
560
|
+
- Updated dependencies [ab04728]
|
|
561
|
+
- @object-ui/types@17.5.0
|
|
562
|
+
|
|
563
|
+
## 17.4.0
|
|
564
|
+
|
|
565
|
+
### Minor Changes
|
|
566
|
+
|
|
567
|
+
- c3b01a7: Give composite and grouped field widgets a real accessible name: the form renderer now associates its label by IDREF for widgets that declare `labelling: 'group'`, instead of emitting a `<label for>` that nothing labelable answers (objectui#3961).
|
|
568
|
+
|
|
569
|
+
Six widgets rendered a visible group label that named **nothing** in the accessibility tree. Measured in a real form, one field per row, reading each label's `for` against the DOM:
|
|
570
|
+
|
|
571
|
+
```
|
|
572
|
+
address for=…-form-item -> MISSING byLabelText=0 role+name=0
|
|
573
|
+
geolocation for=…-form-item -> MISSING byLabelText=0 role+name=0
|
|
574
|
+
checkboxes for=…-form-item -> div byLabelText=0 role+name=0
|
|
575
|
+
radio for=…-form-item -> div[radiogroup] byLabelText=0 role+name=0
|
|
576
|
+
rating for=…-form-item -> div byLabelText=0 role+name=0
|
|
577
|
+
file for=…-form-item -> div[role=button] byLabelText=0 role+name=0
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Two shapes, one outcome. `address` / `geolocation` spread the host's id onto their first sub-input and then replaced it with that input's own unique id (objectui#3343, correct in itself), so the `for` named an id no element carried — clicking "Shipping Address" did nothing and the group label was absent from the accessibility tree entirely. `checkboxes` / `radio` / `rating` / `file` kept the id, but on a `div`: a `<label for>` on a non-labelable element is inert HTML — `HTMLLabelElement.control` is `null`, so it activates nothing and contributes no name. A screen reader heard "Street Address", "City", "Alpha", "Beta" — never which group they belonged to.
|
|
581
|
+
|
|
582
|
+
The fix is the WAI-ARIA group pattern, driven by a DECLARATION rather than by the host guessing at widget DOM:
|
|
583
|
+
|
|
584
|
+
- `@object-ui/core` — `ComponentMeta` gains `labelling?: 'control' | 'group'`. Additive and optional; absent means `'control'`, which is every existing component's behaviour.
|
|
585
|
+
- `@object-ui/components` — the form renderer reads it. For a `'group'` field the `<FormLabel>` publishes an `id` and drops its `for`, and the widget receives `aria-labelledby`. The single-control path is unchanged down to the attribute: no id on the label, no `aria-labelledby` key on the widget, so no field acquires a second naming channel. `ui/form.tsx` is untouched (Shadcn no-touch) — both halves travel as ordinary props, since `<FormLabel>` spreads props after its own `htmlFor`.
|
|
586
|
+
- `@object-ui/fields` — the six audited widgets declare `labelling: 'group'`. `address` / `geolocation` move the host id (and only the id) from their first sub-input to the group container; `checkboxes` / `rating` answer a host-supplied `aria-labelledby` with `role="group"`; `radio` keeps Radix's more specific `radiogroup`; `file` takes the name on its dropzone with no invented group layer, because it has exactly one control that merely happens not to be labelable.
|
|
587
|
+
|
|
588
|
+
No new key in the widget props contract: `aria-*` is already declared on it and forwarded by `toDomProps`, the same channel `aria-required` (objectui#3290) travels.
|
|
589
|
+
|
|
590
|
+
Deliberately unchanged: sub-labels keep naming their own inputs (`aria-labelledby` overrides `<label for>`, so putting the group name on the first sub-input would have replaced "Street Address" with the field name — the concatenated-name outcome this issue rejected), `aria-describedby` stays on the first focusable sub-input where focus can reach it (objectui#3318), the sub-input ids of objectui#3343 do not move, and standalone rendering — the inline grid editor, a bare SDUI node, where nobody hands down an id and there is no host label to point at — emits no role and no IDREF at all.
|
|
591
|
+
|
|
592
|
+
A widget that does not declare itself keeps the old `for`, which the label-association tests report as an association resolving to a non-labelable element. Silence was the failure mode being fixed; the default path stays loud.
|
|
593
|
+
|
|
594
|
+
- 48132f7: Track the `@objectstack` family at `17.0.0-rc.5` (objectui#3560).
|
|
595
|
+
|
|
596
|
+
The pin moves from `^17.0.0-rc.2` to `^17.0.0-rc.5` across all 37 declarations in
|
|
597
|
+
30 `package.json` files, and the sibling `@objectstack/*` packages (`client` /
|
|
598
|
+
`formula` / `lint`) move with it — they pin `@objectstack/spec` **exactly**, so
|
|
599
|
+
leaving them behind would keep a second copy of the spec in the tree and have
|
|
600
|
+
`@objectstack/lint` validating against schemas that still accept the keys rc.3–rc.5
|
|
601
|
+
retire. `pnpm-lock.yaml` now resolves one copy of each of the six family packages
|
|
602
|
+
(`spec` / `client` / `core` / `formula` / `lint` / `sdui-parser`), all at rc.5.
|
|
603
|
+
|
|
604
|
+
Bumping the pin and repairing the fallout cannot be split: the pin alone reddens
|
|
605
|
+
CI, and the code alone targets a shape that is not in effect yet.
|
|
606
|
+
|
|
607
|
+
## A live bug this upgrade fixes
|
|
608
|
+
|
|
609
|
+
**`ObjectStackDataSource.delete()` never emitted its mutation event, and resolved
|
|
610
|
+
`undefined` instead of a boolean.** `@objectstack/client`'s `DeleteDataResult`
|
|
611
|
+
declared a key called `deleted` — a key no schema has ever declared and no server
|
|
612
|
+
path has ever returned on `DELETE /data/:object/:id`. So `result.deleted`
|
|
613
|
+
compiled and read `undefined` at runtime: the guard never fired, a successful
|
|
614
|
+
delete notified no subscriber, and every consumer's cache stayed stale.
|
|
615
|
+
objectstack#5638 corrected the interface to the schema's `success`; following the
|
|
616
|
+
rename is what restores both behaviours. Nothing in this repo had to change shape
|
|
617
|
+
for it — the code was already asking the right question of the wrong key.
|
|
618
|
+
|
|
619
|
+
## Breaking, in FROM → TO form
|
|
620
|
+
|
|
621
|
+
- **The five `@objectstack/spec/ui` interaction-config modules are gone** —
|
|
622
|
+
touch / dnd / keyboard / animation / offline, 32 defs and 64 exports
|
|
623
|
+
(objectstack#4988, PR objectstack#5321). None of them had an authoring door: no
|
|
624
|
+
metadata document could ever carry one of these blocks, so a stack that parsed
|
|
625
|
+
before the retirement parses byte-for-byte the same after it. `@object-ui/types`
|
|
626
|
+
drops the 32 `export type` re-exports. The vocabulary each one's only real
|
|
627
|
+
consumer needs is now declared by that consumer, which is the remedy the spec's
|
|
628
|
+
own retirement ledger prescribes ("declare that union locally — it is your
|
|
629
|
+
client's policy, not the platform's"):
|
|
630
|
+
|
|
631
|
+
- `@object-ui/react`'s `useOffline` owns `OfflineStrategy`, `ConflictResolution`,
|
|
632
|
+
`PersistStorageType`, `EvictionPolicyType`, `OfflineConfig`,
|
|
633
|
+
`OfflineCacheConfig`, `OfflineSyncConfig`;
|
|
634
|
+
- `@object-ui/core`'s `DndProtocol` / `KeyboardProtocol` own `DndConfig`,
|
|
635
|
+
`DragItem`, `DropZone`, `DragConstraint`, `DragHandle`, `DropEffect`,
|
|
636
|
+
`KeyboardNavigationConfig`, `KeyboardShortcut`, `FocusManagement`,
|
|
637
|
+
`FocusTrapConfig`;
|
|
638
|
+
- `@object-ui/types`' `mobile` module owns `SpecGestureConfig`,
|
|
639
|
+
`SwipeGestureConfig`, `PinchGestureConfig`, `LongPressGestureConfig`,
|
|
640
|
+
`TouchTargetConfig`, `TouchInteraction` (plus a new `SPEC_GESTURE_TYPES`
|
|
641
|
+
runtime tuple), so `@object-ui/mobile`'s import paths are unchanged.
|
|
642
|
+
|
|
643
|
+
Every shape is moved verbatim — same keys, same members, same optionality — so
|
|
644
|
+
no hook or bridge changes behaviour. Consumers importing these names from
|
|
645
|
+
`@object-ui/types` must import them from the owning package instead. Note the
|
|
646
|
+
spec's _surviving_ `ConnectorConflictResolution` (`/integration`, connector sync)
|
|
647
|
+
and `ConflictResolutionStrategy` (`/api`, route merge policy) are **different
|
|
648
|
+
concepts** — do not re-point at them.
|
|
649
|
+
|
|
650
|
+
- **`@object-ui/types` no longer re-exports `NotificationAction` or `EmbedConfig`**
|
|
651
|
+
(objectstack#5015, PR objectstack#5300). Both were published `ui` vocabulary with
|
|
652
|
+
no authoring door; no notification action was ever parsed from metadata and no
|
|
653
|
+
iframe route ever read an embed config. The presentation enums
|
|
654
|
+
(`NotificationType` / `NotificationSeverity` / `NotificationPosition`) and
|
|
655
|
+
`SharingConfig` **survive** and are untouched — public form sharing still gates
|
|
656
|
+
the anonymous endpoints on `allowAnonymous` + `publicLink`.
|
|
657
|
+
`@object-ui/core`'s `SharingProtocol` keeps `resolveEmbedConfig` /
|
|
658
|
+
`generateEmbedCode` against a locally declared `EmbedConfig`, so its surface is
|
|
659
|
+
unchanged.
|
|
660
|
+
- **`ThemeEngine` stops emitting nine retired CSS variable groups**
|
|
661
|
+
(objectstack#5021 option 2, PR objectstack#5289). `theme.animation`,
|
|
662
|
+
`theme.zIndex` and five typography groups (`fontSize` / `fontWeight` /
|
|
663
|
+
`lineHeight` / `letterSpacing`, plus `fontFamily.heading` / `fontFamily.mono`)
|
|
664
|
+
are tombstones the schema now rejects by name, so `--duration-*`, `--timing-*`,
|
|
665
|
+
`--z-*`, `--font-size-*`, `--font-weight-*`, `--line-height-*`,
|
|
666
|
+
`--letter-spacing-*`, `--font-heading` and `--font-mono` had become structurally
|
|
667
|
+
dead code — no author could produce the input that reached them.
|
|
668
|
+
`generateAnimationVars` and `generateZIndexVars` are removed from
|
|
669
|
+
`@object-ui/core`, and `@object-ui/types` drops `Animation` / `ZIndex` /
|
|
670
|
+
`AnimationSchema` / `ZIndexSchema`. **`theme.customVars` is the declared — and
|
|
671
|
+
since #5021 the only — door**: each entry is emitted verbatim as
|
|
672
|
+
`--<key>: <value>`, so a `--z-modal` or a `--duration-fast` goes there now.
|
|
673
|
+
LIVE emission is untouched byte for byte: `colors`, `borderRadius`, `shadows`,
|
|
674
|
+
`typography.fontFamily.base` (→ `--font-sans`) and `customVars`.
|
|
675
|
+
- **`@object-ui/types`' `HttpMethodSchema` now binds the spec's
|
|
676
|
+
`HttpMethodSubsetSchema`, and `HttpMethod` binds `HttpMethodSubset`**
|
|
677
|
+
(objectstack#5832, PR objectstack#5976 — objectui#3499). The spec renamed its
|
|
678
|
+
5-value UI subset because `schemaNameFromExportKey` strips the `Schema` suffix,
|
|
679
|
+
so the 5-value and 7-value enums both published as `shared/HttpMethod` and the
|
|
680
|
+
later write won — the emitted JSON Schema and reference page described only one
|
|
681
|
+
of them. **The runtime domain is unchanged and this repo's exported names are
|
|
682
|
+
unchanged**; this follows the rename without touching cross-package semantics.
|
|
683
|
+
Deliberately NOT re-pointed at the spec's bare `HttpMethod`: that is the 7-value
|
|
684
|
+
enum, and widening to it would let `method: 'HEAD'` compile and then throw in
|
|
685
|
+
`HttpRequestSchema.parse()`.
|
|
686
|
+
- **`dashboard.widgets[].actionUrl` / `actionType` / `actionIcon` / `aria` are
|
|
687
|
+
refused, not stripped** (objectstack#5010, ADR-0049 enforce-or-remove). A
|
|
688
|
+
dashboard widget has no action button and never had one — every action the
|
|
689
|
+
dashboard dispatches comes from `header.actions[]` — and no renderer ever applied
|
|
690
|
+
the widget `aria`, so it promised accessibility compliance it did not deliver.
|
|
691
|
+
A stale dashboard now gets a named error telling it where the affordance moved,
|
|
692
|
+
instead of silently losing it. Run `os migrate meta --from 16` to rewrite.
|
|
693
|
+
|
|
694
|
+
### Patch Changes
|
|
695
|
+
|
|
696
|
+
- 6719877: `condition: false` now actually prevents the action from executing (objectui#3872)
|
|
697
|
+
|
|
698
|
+
`ActionRunner.execute` — the engine's public execution entry, shared by every
|
|
699
|
+
action surface — gated conditional execution on `if (action.condition)`, i.e. on
|
|
700
|
+
the raw value's TRUTHINESS. Truthiness cannot answer the question the gate needs
|
|
701
|
+
answered ("did the author declare a condition?"), and on this key it answered in
|
|
702
|
+
the over-permissive direction: `condition: false` fell on `if (false)`, so the
|
|
703
|
+
whole block was skipped, `evaluateCondition` was never consulted, and the action
|
|
704
|
+
executed. Measured with a call-counting handler:
|
|
705
|
+
|
|
706
|
+
- `condition: false` — handler ran: **true**, result `{ success: true }`
|
|
707
|
+
- `condition: { dialect: 'cel', source: 'false' }` — handler ran: false, result `{ success: false, error: 'Action condition not met' }`
|
|
708
|
+
|
|
709
|
+
Two spellings of the same statement, opposite outcomes. `false` is the most
|
|
710
|
+
explicit "never execute this" metadata can carry — and what a template that
|
|
711
|
+
switches an action off emits — so the direction matters: the action really ran,
|
|
712
|
+
possibly writing. This is the over-permission half of the objectui#3492 family
|
|
713
|
+
(the `disabled` gate one line below is objectui#3848, whose defect pointed the
|
|
714
|
+
other way).
|
|
715
|
+
|
|
716
|
+
The gate now asks whether a `condition` gate is DECLARED before evaluating.
|
|
717
|
+
"Nothing to evaluate" is read from core's single predicate normalizer
|
|
718
|
+
(`toPredicateInput`, which maps `''`, `null`, an empty-`source` envelope and
|
|
719
|
+
non-predicate values to `undefined`), plus the whitespace-only string, which the
|
|
720
|
+
normalizer wraps rather than collapses — objectui#3850's ruling on the scope of
|
|
721
|
+
"empty predicate", the same one the `disabled` gate already applies. Once the
|
|
722
|
+
door asks the right question the verdict needs no boolean branch of its own:
|
|
723
|
+
`evaluateCondition` returns a boolean argument verbatim.
|
|
724
|
+
|
|
725
|
+
**Behaviour change surface, deliberately one-directional and one row wide.**
|
|
726
|
+
Exactly one shape changes verdict — a declared boolean `false`, from executing to
|
|
727
|
+
refused (`{ success: false, error: 'Action condition not met' }`, the message the
|
|
728
|
+
key already used). Everything else is byte-identical: `condition: true`, an
|
|
729
|
+
absent `condition`, and truthy expressions/envelopes still execute; falsy
|
|
730
|
+
expressions, falsy CEL envelopes and falsy `${…}` templates are still refused,
|
|
731
|
+
as before; the three empty predicates (`''`, whitespace-only, an empty-`source`
|
|
732
|
+
envelope) still execute, now because nothing was declared rather than because
|
|
733
|
+
`if ('')` happened to be falsy; and non-predicate junk (`0`, `{}`) still
|
|
734
|
+
executes — a value that is not a predicate must not decide an action's fate,
|
|
735
|
+
which is the fail-open posture this module already committed to for `disabled`.
|
|
736
|
+
So this change can only start refusing execution, never start allowing it — the
|
|
737
|
+
mirror image of objectui#3848's fix.
|
|
738
|
+
|
|
739
|
+
`ActionDef.condition` is widened to `string | boolean` to match what the gate now
|
|
740
|
+
honours (and the `disabled` key beside it). This is not a lenient consumer
|
|
741
|
+
alias: the boolean was always accepted at runtime through the interface's index
|
|
742
|
+
signature, it was simply ignored.
|
|
743
|
+
|
|
744
|
+
The value handed to the evaluator is deliberately left RAW rather than normalized
|
|
745
|
+
first, for the same reason as objectui#3848 with the sign flipped:
|
|
746
|
+
`toPredicateInput` wraps unconditionally, so an already-templated `'${x}'`
|
|
747
|
+
becomes `'${${x}}'`, fails to parse, returns verbatim and coerces to a constant
|
|
748
|
+
`true` — on `disabled` that blocks everything, on `condition` it would EXECUTE
|
|
749
|
+
everything. That normalizer defect is objectui#3871; a tripwire next to the new
|
|
750
|
+
pins goes red the day it is fixed.
|
|
751
|
+
|
|
752
|
+
- 56ff091: An empty `disabled` predicate no longer refuses to run the action (objectui#3848)
|
|
753
|
+
|
|
754
|
+
`ActionRunner.execute` — the engine's public execution entry, shared by every
|
|
755
|
+
action surface — gated on `action.disabled != null && action.disabled !== false`
|
|
756
|
+
and handed the value straight to `evaluateCondition`. That function documents one
|
|
757
|
+
default for "there is no condition here": return `true`, meaning
|
|
758
|
+
_visible/enabled_. On `disabled`, `true` means BLOCKED. So every empty predicate
|
|
759
|
+
was read as "disabled": the handler was never invoked and the caller got
|
|
760
|
+
`{ success: false, error: 'Action is disabled' }` — a state the metadata never
|
|
761
|
+
declared. Measured with a call-counting handler:
|
|
762
|
+
|
|
763
|
+
- `disabled: ''` — handler ran: false
|
|
764
|
+
- `disabled: ' '` (whitespace only) — handler ran: false
|
|
765
|
+
- `disabled: { dialect: 'cel', source: '' }` (the empty envelope `objectstack build` can emit) — handler ran: false
|
|
766
|
+
|
|
767
|
+
After objectui#3842 / objectui#3849 fixed the renderer halves, this was live
|
|
768
|
+
user-visible behaviour: the button became clickable and clicking it returned
|
|
769
|
+
`Action is disabled` — the renderer and the execution entry disagreeing about one
|
|
770
|
+
predicate value, the shape objectui#3314 already paid for once.
|
|
771
|
+
|
|
772
|
+
The gate now asks whether a `disabled` gate is DECLARED — whether there is a
|
|
773
|
+
condition to reach a verdict on — before evaluating. "Nothing to evaluate" is
|
|
774
|
+
read from core's single predicate normalizer (`toPredicateInput`, which maps
|
|
775
|
+
`''`, `null`, an empty-`source` envelope and non-predicate values to
|
|
776
|
+
`undefined`), plus the whitespace-only string, which the normalizer wraps rather
|
|
777
|
+
than collapses and which `evaluateCondition` itself calls "no condition"
|
|
778
|
+
(`evalRowPredicate` applies the same blank-source rule).
|
|
779
|
+
|
|
780
|
+
**Behaviour change surface, deliberately one-directional.** Only values with
|
|
781
|
+
nothing to evaluate change, and only from blocked to allowed: `''`,
|
|
782
|
+
whitespace-only, an empty-`source` envelope, and non-predicate junk (`0`, `{}`,
|
|
783
|
+
which previously coerced to "disabled"). `disabled: true`, a truthy expression
|
|
784
|
+
and a truthy CEL envelope still block; `disabled: false` and an absent `disabled`
|
|
785
|
+
still run; no expression- or envelope-valued predicate changes verdict. The
|
|
786
|
+
existing `catch { isDisabled = false }` fail-open posture is untouched, and this
|
|
787
|
+
change can only stop blocking things, never start.
|
|
788
|
+
|
|
789
|
+
The value handed to the evaluator is deliberately left RAW rather than normalized
|
|
790
|
+
first. `evaluateCondition(toPredicateInput(x))` is not interchangeable with
|
|
791
|
+
`evaluateCondition(x)` for a string that is already a `${…}` template:
|
|
792
|
+
`toPredicateInput` assumes a bare expression and wraps unconditionally, so
|
|
793
|
+
`'${x}'` becomes `'${${x}}'`, fails to parse, returns verbatim, and coerces to a
|
|
794
|
+
constant `true` — a template-spelled predicate evaluated that way is ALWAYS
|
|
795
|
+
"disabled", whatever it says. That normalizer defect is filed as objectui#3871
|
|
796
|
+
(it is live at the action renderers and `ActionEngine`, while `SchemaRenderer` and
|
|
797
|
+
`page:header` evaluate the raw value and pin the correct verdict); a tripwire next
|
|
798
|
+
to the new pins goes red the day it is fixed. Two rows therefore still differ
|
|
799
|
+
between the execution and renderer paths, each recorded with its owning issue:
|
|
800
|
+
the empty envelope (objectui#3850 owns the renderer half's scope ruling) and the
|
|
801
|
+
`${…}` spelling (objectui#3871).
|
|
802
|
+
|
|
803
|
+
- 4bc6c23: Converge dashboard widget `compareTo` on the executor's `{ kind, dimension? }` contract, and make the dataset path actually render a comparison
|
|
804
|
+
|
|
805
|
+
`CompareToConfig` was a three-branch union (`'previousPeriod' | 'previousYear' | { offset }`). `@objectstack/spec` collapsed it to the shape the analytics executor already implements — `DatasetCompareTo`, a plain strict object `{ kind: 'previousPeriod' | 'previousYear'; dimension?: string }` (objectstack#5011) — so this renderer now reads that one shape:
|
|
806
|
+
|
|
807
|
+
- `shiftFilterByCompareTo` / `compareToTrendLabelKey` dispatch on `compareTo.kind`. The `{ offset }` duration shift is gone: `{ offset: '1y' }` is `kind: 'previousYear'`, while `'7d'` / `'1M'` have no faithful target and are restated by the author on the widget's own `filter` plus `kind: 'previousPeriod'`. No trend label key is retired — the offset arm resolved to `vsPreviousPeriod`, which survives as the `previousPeriod` fallback.
|
|
808
|
+
- `DatasetWidget` no longer discards part of `compareTo`. It used to forward only the object form because the two string forms had no meaning downstream; with one shape there is nothing to discard, and a stale string is now invalid metadata rejected where it is authored rather than silently reinterpreted here.
|
|
809
|
+
- **The comparison now actually runs on the dataset path.** A widget states its window in its own `filter` (a date macro, or the dashboard date-range filter merged in), but the executor shifts a `timeDimensions` entry carrying a `dateRange` — so a dataset widget asking for a comparison got "compareTo needs a dated window to shift" and rendered none. When (and only when) a comparison is requested, the resolved filter's bounded date windows are lowered into `selection.timeDimensions[].dateRange` and moved out of `runtimeFilter` (a copy left behind would intersect the shifted window with the current one and empty every comparison column). Which dimension gets shifted stays the executor's decision: every window found is lowered under the name the author wrote, and zero or two candidates surfaces the executor's own error, listing them.
|
|
810
|
+
- The `<measure>__compare` columns that come back are now shown: a delta + window label on KPI widgets, a comparison column on tables, and a `variant: 'comparison'` overlay series on charts — the same treatment and the same `dashboard.trend.*` labels the inline object-provider widgets already use.
|
|
811
|
+
|
|
812
|
+
- e06810e: `PageComponentSchema.dataSource` is now consumed instead of discarded — a
|
|
813
|
+
`list-view` page component can reference a **saved view by name** for the first
|
|
814
|
+
time, and writing the binding no longer breaks the component
|
|
815
|
+
(objectstack#5576).
|
|
816
|
+
|
|
817
|
+
The spec declares a per-element data binding on every page component —
|
|
818
|
+
`dataSource: { object, view?, filter?, sort?, limit? }` — and objectui read none
|
|
819
|
+
of it. `ViewDataProvider.resolveElementDataSource` forwarded
|
|
820
|
+
`filter`/`sort`/`limit` and dropped `view` entirely, and had no caller outside its
|
|
821
|
+
own test; nothing mapped `object` onto the `objectName` a list actually reads. So
|
|
822
|
+
"reference a saved view by name" was published, validated and inert, and every
|
|
823
|
+
page that wanted a saved view's columns/filter/sort had to inline a second copy of
|
|
824
|
+
them — the drift the binding exists to remove.
|
|
825
|
+
|
|
826
|
+
Writing the binding also **broke** the block, for a reason unrelated to `view`:
|
|
827
|
+
`SchemaRenderer` spread the schema's `dataSource` metadata onto the component as a
|
|
828
|
+
React prop, and that is the prop name the host uses to inject the data-source
|
|
829
|
+
ADAPTER. The plain `{ object, view }` object shadowed the adapter, so the first
|
|
830
|
+
`dataSource.find(…)` threw `dataSource.find is not a function` and `list-view`
|
|
831
|
+
rendered "Couldn't load records" — a spec-compliant component failing next to
|
|
832
|
+
identical ones that omitted the binding.
|
|
833
|
+
|
|
834
|
+
- `@object-ui/react` — `SchemaRenderer` no longer spreads `schema.dataSource` as a
|
|
835
|
+
prop (it is metadata, like `visibleWhen`); renderers read it off `schema`. An
|
|
836
|
+
explicit React `dataSource` prop is unaffected. New
|
|
837
|
+
`useElementDataSource(schema, dataSource?)` hook resolves a binding, fetching
|
|
838
|
+
the named saved view from the object definition's `listViews` and the metadata
|
|
839
|
+
overlay's `listViews()`.
|
|
840
|
+
- `@object-ui/core` — new `isElementDataSourceConfig` / `collectSavedViews` /
|
|
841
|
+
`resolveSavedView` / `composeElementDataSource`, and `resolveElementDataSource`
|
|
842
|
+
now honours `view` through an optional `DataFetcher.fetchViews`, reporting an
|
|
843
|
+
unresolvable view as an error instead of silently returning every record.
|
|
844
|
+
`resolveViewId` moved here from `@object-ui/app-shell` (re-exported there) so
|
|
845
|
+
one matcher serves both the object page and a page component.
|
|
846
|
+
- `@object-ui/plugin-list` — `list-view` maps the binding onto the props
|
|
847
|
+
`ListView` reads. `dataSource.*` keys are authoritative, view-supplied values
|
|
848
|
+
are a baseline the component's own keys override, and `filter` AND-combines at
|
|
849
|
+
every level (the spec calls the binding's filter "additional criteria"), so a
|
|
850
|
+
binding can narrow a saved view but never widen it. A `view` name that does not
|
|
851
|
+
resolve renders a configuration error naming the object's actual views and
|
|
852
|
+
issues no query — it never falls back to the object's default view, because that
|
|
853
|
+
turns a typo into a silently wider answer.
|
|
854
|
+
|
|
855
|
+
- ab3ad4f: An empty predicate is no longer a declared gate anywhere (objectui#3850, objectui#3862)
|
|
856
|
+
|
|
857
|
+
"Is a gate DECLARED on this key — is there a condition to reach a verdict on?" was
|
|
858
|
+
answered three times in this repo, with three different scopes, and the widest
|
|
859
|
+
answers sat on `disabled`, where the mistake is not benign:
|
|
860
|
+
|
|
861
|
+
- `hasDeclaredVisibilityGate` (the action face) asked `!= null && !== ''`, so every
|
|
862
|
+
OBJECT counted — including `{ dialect: 'cel', source: '' }`. That envelope is not
|
|
863
|
+
a hand-written curiosity: `@objectstack/spec`'s `ExpressionInputSchema` normalizes
|
|
864
|
+
every authored predicate into one, so "the author left the predicate empty"
|
|
865
|
+
compiles to exactly it. The verdict path normalized the same value back to
|
|
866
|
+
`undefined`, and `evaluateCondition(undefined)` answers `true` — "no condition, so
|
|
867
|
+
visible/enabled". On `visible` that `true` means SHOW, so the two mistakes
|
|
868
|
+
cancelled; on `disabled` it means GREY, so they compounded: a button disabled
|
|
869
|
+
forever that no author asked to disable (objectui#3850, the residue objectui#3842
|
|
870
|
+
left behind).
|
|
871
|
+
- `SchemaRenderer` asked `disabled !== undefined` inline, one notch wider again, so
|
|
872
|
+
`disabled: null` greyed out too — on the GENERIC rendering path, since that block
|
|
873
|
+
runs for every node type, and not as an internal flag either: `_disabled` is
|
|
874
|
+
forwarded to the component as a real `disabled` prop (objectui#3862).
|
|
875
|
+
- `ActionRunner`'s execution gates asked "does this normalize to something
|
|
876
|
+
evaluable?" — the scope that turned out to be right (objectui#3848 / objectui#3872).
|
|
877
|
+
|
|
878
|
+
There is now ONE definition, `hasDeclaredPredicate`, exported from
|
|
879
|
+
`@object-ui/core` (`evaluator/declaredPredicate.ts`, beside the `toPredicateInput`
|
|
880
|
+
normalizer it is derived from): a gate is declared when normalization still leaves a
|
|
881
|
+
condition to evaluate. `''`, a whitespace-only string, an empty-`source` envelope
|
|
882
|
+
and any non-predicate value (`0`, `{}`) are NOT declared; `false` IS (a verdict is
|
|
883
|
+
not a missing gate — objectui#3812). `hasDeclaredVisibilityGate` keeps its name as a
|
|
884
|
+
re-export of it, so the five member-action renderer call sites, `DeclaredActionsBar`
|
|
885
|
+
and `record-quick-actions` are unchanged and inherit the scope;
|
|
886
|
+
`SchemaRenderer`'s `disabled` / `disabledOn` chain and `ActionRunner`'s two gates
|
|
887
|
+
read the same function. No consumer got a local "and also check for empty" test —
|
|
888
|
+
that fourth dialect is what objectui#3842 / objectui#3849 spent two PRs merging away.
|
|
889
|
+
|
|
890
|
+
Measured behaviour change, `action:button` and the generic path, before → after:
|
|
891
|
+
|
|
892
|
+
| value | `visible` | `disabled` | `enabled` | `SchemaRenderer` `disabled` prop |
|
|
893
|
+
| --------------------------------------------------------- | -------------- | ---------- | --------- | -------------------------------- |
|
|
894
|
+
| `''` | shown → shown | on → on | on → on | forwarded → absent |
|
|
895
|
+
| `null` | shown → shown | on → on | on → on | forwarded → absent |
|
|
896
|
+
| `{ dialect: 'cel', source: '' }` | shown → shown | GREY → on | on → on | forwarded → absent |
|
|
897
|
+
| `{ source: '' }` | shown → shown | GREY → on | on → on | forwarded → absent |
|
|
898
|
+
| `' '` (whitespace) | HIDDEN → shown | on → on | GREY → on | forwarded → absent |
|
|
899
|
+
| `0` / `{}` (not predicates) | shown → shown | GREY → on | on → on | forwarded → absent |
|
|
900
|
+
| `true` / `false` / bare CEL / `${…}` / non-empty envelope | unchanged | unchanged | unchanged | unchanged |
|
|
901
|
+
|
|
902
|
+
Every row moves toward "there is no gate here", never away from it, and no value
|
|
903
|
+
that HAS a verdict changes it — the verdict is still read from the raw value, only
|
|
904
|
+
the gate in front of it narrowed. Two rows are behaviour changes rather than the
|
|
905
|
+
equivalence the ruling expected, and are pinned as such: the whitespace string moves
|
|
906
|
+
on `visible` / `enabled` (it used to normalize to `'${ }'`, which evaluates falsy,
|
|
907
|
+
so a predicate that says nothing HID the action from everyone), and non-predicate
|
|
908
|
+
junk stops greying controls out (fail-open, the posture `ActionRunner` already
|
|
909
|
+
committed to).
|
|
910
|
+
|
|
911
|
+
One blank spelling is knowingly still outside the scope: an envelope whose `source`
|
|
912
|
+
is blank but not EMPTY (`{ dialect: 'cel', source: ' ' }`) — the normalizer folds a
|
|
913
|
+
`source` of `''` and does not trim, so the string spelling of a blank predicate is
|
|
914
|
+
trimmed and the envelope spelling is not, and `disabled` still greys out for that one
|
|
915
|
+
value. The ruling enumerated three empty spellings; this is a fourth, measured and
|
|
916
|
+
filed as objectui#3960 rather than widened in here.
|
|
917
|
+
|
|
918
|
+
One chain is deliberately untouched: `SchemaRenderer`'s `visible` / `visibleWhen` /
|
|
919
|
+
`visibleOn` / `visibility` / `hidden` / `hiddenOn` legs keep `!== undefined`, because
|
|
920
|
+
narrowing them would change ALIAS PRECEDENCE, not just emptiness. The `hidden` legs
|
|
921
|
+
are not negated and therefore carry this same defect with the polarity that makes the
|
|
922
|
+
node vanish — measured, out of this ruling's scope, filed as objectui#3955.
|
|
923
|
+
|
|
924
|
+
- c2fd122: fix(actions): forward `bodyShape` end-to-end so a declared body wrap is honoured
|
|
925
|
+
|
|
926
|
+
Sibling of the `bodyExtra` fix, same failure shape one key over. `bodyShape` is
|
|
927
|
+
the spec's body-WRAPPING declaration for a `type: 'api'` action — `'flat'` (the
|
|
928
|
+
default) or `{ wrap: key }` to nest the collected params under `key`, the shape
|
|
929
|
+
better-auth's `organization/update` needs. The console `apiHandler` read it
|
|
930
|
+
unconditionally while **no** action renderer forwarded it, so an author who
|
|
931
|
+
declared `bodyShape: { wrap: 'data' }` on an `action:button` / `:group` / `:icon`
|
|
932
|
+
/ `:menu` action got a FLAT body on the wire: the endpoint received the params at
|
|
933
|
+
the top level, and the declaration read as honoured because it parsed and
|
|
934
|
+
published.
|
|
935
|
+
|
|
936
|
+
The four declared-action renderers now forward the key, and `ActionSchema`
|
|
937
|
+
declares it (typed by derivation from the spec, so the union cannot drift).
|
|
938
|
+
`ActionRunner.executeAPI` — the fallback path taken when no host registered an
|
|
939
|
+
`api` handler — now reads it too, closing a second asymmetry in which the same
|
|
940
|
+
action changed body shape depending on which host executed it. The wrap covers
|
|
941
|
+
the collected params only; `bodyExtra` and other top-level keys stay flat, which
|
|
942
|
+
is the spec's own wording for the key and what both console read-sites already
|
|
943
|
+
did.
|
|
944
|
+
|
|
945
|
+
`element:button` deliberately does **not** forward it: its whitelist mirrors
|
|
946
|
+
spec's `InlineActionSchema` pick list field for field, and that pick list does
|
|
947
|
+
not include `bodyShape` — it is not inline vocabulary.
|
|
948
|
+
|
|
949
|
+
- 1d723e3: fix(core): stop re-wrapping an already-`${…}` predicate, so action-face `visible` / `disabled` finally honour it (objectui#3871)
|
|
950
|
+
|
|
951
|
+
`toPredicateInput` — the one normalizer every action surface and the action
|
|
952
|
+
engine share — wrapped **every** string as `${string}`, assuming a bare
|
|
953
|
+
expression. But `${…}` is a spelling this repo documents for a predicate
|
|
954
|
+
(AGENTS.md §4) and one the normalizer's own output type lists as valid, so an
|
|
955
|
+
already-normalized value was wrapped a second time: `'${x}'` became `'${${x}}'`,
|
|
956
|
+
which cannot match the evaluator's single-template fast path and does not parse.
|
|
957
|
+
The author's expression then decided nothing, and which constant came back
|
|
958
|
+
depended on the caller's error policy:
|
|
959
|
+
|
|
960
|
+
- **fail-soft** legs (every `disabled` / `enabled` leg; `visible` on
|
|
961
|
+
`action:icon`, `action:group` and the related-list toolbar) got the unparsed
|
|
962
|
+
string back, so `Boolean(…)` was a constant `true`: `disabled: '${…}'` greyed
|
|
963
|
+
the action out permanently, `visible: '${…}'` showed one the author had gated
|
|
964
|
+
away, and `enabled: '${…}'` never disabled anything.
|
|
965
|
+
- **fail-closed** legs (`throwOnError: true` — `visible` on `action:button`,
|
|
966
|
+
`action:bar`, `action:menu`, `DeclaredActionsBar`, and
|
|
967
|
+
`ActionEngine.getActionsForLocation`) got a **throw**, which each site turns
|
|
968
|
+
into "hidden": an action gated with a template predicate was invisible even
|
|
969
|
+
while its gate held, and the fail-closed warning blamed the author's
|
|
970
|
+
expression.
|
|
971
|
+
|
|
972
|
+
A string that already carries `${` is now returned untouched (the same guard
|
|
973
|
+
covers the envelope branch, where an unwrapped `source` reaches the identical
|
|
974
|
+
wrap), which makes the normalizer idempotent. Every affected surface goes from a
|
|
975
|
+
constant verdict to the predicate's real one — converging the action face with
|
|
976
|
+
`SchemaRenderer` and `page:header`, which read the raw value and have always
|
|
977
|
+
been right about this spelling (objectui#3314's shape). Bare expressions and
|
|
978
|
+
`{ dialect: 'cel' }` envelopes are untouched.
|
|
979
|
+
|
|
980
|
+
`ActionRunner`'s two execution gates already read the raw value and are
|
|
981
|
+
unchanged; the objectui#3871 tripwires they carried have been replaced by pins
|
|
982
|
+
of the now-converged behaviour.
|
|
983
|
+
|
|
984
|
+
Whether `${…}` should be _authorable_ on an action predicate at all is a
|
|
985
|
+
separate, spec-side question (`@objectstack/spec`'s `PredicateInput` models a
|
|
986
|
+
bare string and a dialect envelope): if it is to be rejected, that belongs in
|
|
987
|
+
publish-time validation, not in a consumer that silently invents a verdict.
|
|
988
|
+
|
|
989
|
+
- 0109f54: Blank predicates and non-predicate values are no longer gates, at the last three entries that still judged them (objectui#3955, objectui#3957, objectui#3960)
|
|
990
|
+
|
|
991
|
+
objectui#3850 sank "is a predicate gate DECLARED here?" into one definition,
|
|
992
|
+
`@object-ui/core`'s `hasDeclaredPredicate`. Three places were left out of that
|
|
993
|
+
ruling's placement clause, each with the same shape of defect: the evaluator's
|
|
994
|
+
single default for "there is nothing here to evaluate" is `true`, meaning
|
|
995
|
+
_visible/enabled_, and wherever a too-wide "declared" test hands it an empty
|
|
996
|
+
predicate on an INVERTED key, that `true` turns a control off for a value the
|
|
997
|
+
metadata never used to say anything.
|
|
998
|
+
|
|
999
|
+
**`SchemaRenderer`'s `hidden` / `hiddenOn` legs (objectui#3955)** asked
|
|
1000
|
+
`!== undefined` and did NOT negate the verdict, so an empty predicate meant HIDE
|
|
1001
|
+
and the node disappeared — on the generic rendering path, since that block runs
|
|
1002
|
+
for every schema type. Harder to diagnose than the `disabled` twin objectui#3862
|
|
1003
|
+
fixed: a greyed-out control is still on screen, while a node that never rendered
|
|
1004
|
+
is indistinguishable from metadata that meant to hide it. Both legs now read the
|
|
1005
|
+
shared definition.
|
|
1006
|
+
|
|
1007
|
+
**The "blank" criterion now covers the envelope spelling (objectui#3960).** The
|
|
1008
|
+
definition trimmed a whitespace-only STRING and not an envelope's whitespace-only
|
|
1009
|
+
`source`, because `toPredicateInput` folds a `source` of `''` and does not trim.
|
|
1010
|
+
So `{ dialect: 'cel', source: ' ' }` was a declared gate whose verdict came from
|
|
1011
|
+
core's own CEL entry calling that exact value "no predicate" (`if (!source.trim())
|
|
1012
|
+
return true`) — `disabled` greyed out forever and `ActionRunner.execute` answered
|
|
1013
|
+
`{ success: false, error: 'Action is disabled' }` with the handler never invoked.
|
|
1014
|
+
Blankness is now decided once for both spellings, at the definition. The
|
|
1015
|
+
NORMALIZER's contract is deliberately unchanged: "what shape does the evaluator
|
|
1016
|
+
accept" is not the same question as "is there a condition", and moving the trim
|
|
1017
|
+
there would have flipped verdicts for every
|
|
1018
|
+
`useCondition(toPredicateInput(…))` call site, including container-level `visible`
|
|
1019
|
+
reads that never asked this question at all.
|
|
1020
|
+
|
|
1021
|
+
**`ActionEngine.getActionsForLocation`'s `visible` filter (objectui#3957)** was the
|
|
1022
|
+
last consumer answering the question with a range of its own — three empty
|
|
1023
|
+
spellings folded by hand, everything else coerced with `Boolean(raw)`. It now reads
|
|
1024
|
+
the shared definition and the coercion branch is gone, so one value no longer gets
|
|
1025
|
+
two answers depending on whether an action was surfaced by the engine or rendered
|
|
1026
|
+
standalone (the invariant objectui#3314 established). Its fail-CLOSED posture on a
|
|
1027
|
+
predicate that THROWS is untouched (`throwOnError: true` + `warnHiddenPredicate`):
|
|
1028
|
+
"the predicate faulted" and "there is no predicate" are different facts.
|
|
1029
|
+
|
|
1030
|
+
Behaviour changes, before → after. Observation-class: each needs an author to write
|
|
1031
|
+
an empty/blank predicate or a non-predicate value, and there is no known user path
|
|
1032
|
+
today.
|
|
1033
|
+
|
|
1034
|
+
| value | `ActionEngine` `visible` | `SchemaRenderer` `hidden` | `disabled` (action face + generic path) | `ActionRunner.execute` `disabled` |
|
|
1035
|
+
| ----------------------------------------------------------- | ------------------------ | ------------------------- | --------------------------------------- | --------------------------------- |
|
|
1036
|
+
| `''` / `null` | shown → shown | HIDDEN → rendered | unchanged | unchanged |
|
|
1037
|
+
| `' '` (blank text) | HIDDEN → shown | HIDDEN → rendered | unchanged | unchanged |
|
|
1038
|
+
| `0` / `NaN` | HIDDEN → shown | HIDDEN → rendered | unchanged | unchanged |
|
|
1039
|
+
| `{}` / `[]` | shown → shown | HIDDEN → rendered | unchanged | unchanged |
|
|
1040
|
+
| `{ dialect: 'cel', source: '' }` | shown → shown | HIDDEN → rendered | unchanged | unchanged |
|
|
1041
|
+
| `{ dialect: 'cel', source: ' ' }` | shown → shown | HIDDEN → rendered | GREY → on | refused → runs |
|
|
1042
|
+
| `{ source: ' ' }` (no dialect) | HIDDEN → shown | HIDDEN → rendered | GREY → on | refused → runs |
|
|
1043
|
+
| `true` / `false` / bare CEL / `${…}` / a non-blank envelope | unchanged | unchanged | unchanged | unchanged |
|
|
1044
|
+
|
|
1045
|
+
Every row moves toward "there is no gate here", never away from it, and no value
|
|
1046
|
+
that HAS a verdict changes it — a declared `false` is still a verdict, not a
|
|
1047
|
+
missing gate (objectui#3812), and blankness is `trim()`, not "short": `{ dialect:
|
|
1048
|
+
'cel', source: ' x ' }` is a predicate. One alias precedence changes with the
|
|
1049
|
+
`hidden` legs and is pinned rather than claimed as an equivalence: an undeclared
|
|
1050
|
+
`hidden` no longer short-circuits the chain, so a declared `hiddenOn` is finally
|
|
1051
|
+
consulted.
|
|
1052
|
+
|
|
1053
|
+
`SchemaRenderer`'s four `visible*` legs keep `!== undefined` deliberately, as
|
|
1054
|
+
objectui#3850 ruled: their `true` is negated, so an empty predicate already lands
|
|
1055
|
+
on "shown", and narrowing them would change alias precedence rather than fix
|
|
1056
|
+
anything.
|
|
1057
|
+
|
|
1058
|
+
- 7e5bb5d: fix(actions): forward `bodyExtra` end-to-end through the action chain
|
|
1059
|
+
|
|
1060
|
+
An action's static request body (`bodyExtra`) was dropped one hop before the
|
|
1061
|
+
`ActionRunner`: every action renderer forwards an explicit whitelist of keys, and
|
|
1062
|
+
none of them listed `bodyExtra`. Since `@objectstack/spec` 17 made it the only way
|
|
1063
|
+
a `type: 'api'` action can carry a payload (`params` keeps its single meaning as
|
|
1064
|
+
the parameter definition array), and the ADR-0087
|
|
1065
|
+
`inline-action-api-params-to-body-extra` conversion rewrites older object-form
|
|
1066
|
+
`params` pages onto it at load, a previously-working published page validated,
|
|
1067
|
+
published and then POSTed an empty body.
|
|
1068
|
+
|
|
1069
|
+
`element:button`, `action:button`, `action:group`, `action:icon` and `action:menu`
|
|
1070
|
+
now forward the key; `ActionRunner.executeAPI` merges it into the request body
|
|
1071
|
+
**last** (so a constant always overrides a same-named user param, matching the
|
|
1072
|
+
console `apiHandler`); `ActionSchema` declares it; and a non-array `params` on a
|
|
1073
|
+
`type: 'api'` action keeps working for one version window with a dev-mode
|
|
1074
|
+
deprecation warning naming `bodyExtra`.
|
|
1075
|
+
|
|
1076
|
+
- fbc23e0: Action params that inherit a field's options now keep the keys that field declared
|
|
1077
|
+
|
|
1078
|
+
A field-backed action param (`{ field: 'tier' }`) had its inherited option list
|
|
1079
|
+
rebuilt entry by entry as `{ label, value }`, which silently dropped every other
|
|
1080
|
+
key the field's options declared — most consequentially the per-option
|
|
1081
|
+
`visibleWhen` predicate (ADR-0058). A select field whose options narrow by
|
|
1082
|
+
predicate in an object form therefore offered the FULL list in an action dialog,
|
|
1083
|
+
including the entries the predicate exists to hide, with no diagnostic on either
|
|
1084
|
+
side; `color` / `icon` / `disabled` were lost the same way. Options authored
|
|
1085
|
+
inline on the param were never affected — they always passed through verbatim,
|
|
1086
|
+
which is the asymmetry this restores.
|
|
1087
|
+
|
|
1088
|
+
The resolver now preserves each inherited entry and only does its two real jobs:
|
|
1089
|
+
expanding bare strings into label/value pairs and translating the label through
|
|
1090
|
+
`fieldOptionLabel`. The option widgets already filter on `visibleWhen`, so a
|
|
1091
|
+
role-gated option (`'admin' in current_user.positions`) inherited by a dialog
|
|
1092
|
+
param now narrows the offered set and clears a seeded value the predicate hides.
|
|
1093
|
+
|
|
1094
|
+
`ActionParamDef.options` (`@object-ui/core`) and the resolver's `RawActionParam`
|
|
1095
|
+
are widened to match: `ActionParamOption` names the two keys the param layer
|
|
1096
|
+
reads and carries the rest of a field's option vocabulary through.
|
|
1097
|
+
|
|
1098
|
+
- 6bb454a: `evalRowPredicate`: the fail-closed report now names the engine's failure reason, and the ROW always wins over host scope (objectui#3792, objectui#3796)
|
|
1099
|
+
|
|
1100
|
+
Two defects in one function, `packages/core/src/evaluator/listConditional.ts`,
|
|
1101
|
+
shared by every surface that gates on a row predicate: the row kebab, the bulk
|
|
1102
|
+
selection bar, kanban conditional formatting, and — since objectui#3521 — the
|
|
1103
|
+
record page header.
|
|
1104
|
+
|
|
1105
|
+
**The safest path said the least (objectui#3792).** `evalRowPredicate` has two
|
|
1106
|
+
diagnostic routes, and their information content was inverted. The single-eval
|
|
1107
|
+
fast route lets the canonical helper warn, so it prints the engine's own reason
|
|
1108
|
+
(`Reason: [runtime] No such key: owner_id`). The `warnOnError: true` route — the
|
|
1109
|
+
fail-CLOSED one, taken by every caller that makes a button disappear — runs
|
|
1110
|
+
`evalFieldPredicate` twice with `{ warn: false }` to tell a fault from a genuine
|
|
1111
|
+
`false`, and that silence discarded the reason along with the duplicate warning.
|
|
1112
|
+
So the more decisively a surface hid a control, the less it said about why: the
|
|
1113
|
+
console line named the predicate but not the defect, and `No such key: owner_id`
|
|
1114
|
+
or `no such overload: string == null` is usually the whole answer.
|
|
1115
|
+
|
|
1116
|
+
`FieldPredicateDiagnostic` gains an optional `onFault(reason)` passback:
|
|
1117
|
+
`evalFieldPredicate` hands out the engine's verbatim text (kind tag, message and
|
|
1118
|
+
source excerpt — the exact string it prints after `Reason:`) even when its own
|
|
1119
|
+
warning is suppressed. It fires per fault, deliberately independent of the
|
|
1120
|
+
once-per-predicate warning dedupe, so a caller doing its own warn-once
|
|
1121
|
+
bookkeeping keeps control of it; the verdict is unaffected, and callers that pass
|
|
1122
|
+
nothing are unchanged. `evalRowPredicate` threads that reason into its labelled
|
|
1123
|
+
warning, and the legacy-dialect route does the same with the message its engine
|
|
1124
|
+
throws (`Reason: [legacy] …`), so both dialect paths report a fault the same way.
|
|
1125
|
+
Warn-once semantics are unchanged — the reason is deliberately not part of the
|
|
1126
|
+
dedupe key.
|
|
1127
|
+
|
|
1128
|
+
**Wording: not a list function, and not for a long time.** The same message
|
|
1129
|
+
called every caller a "**list** conditional predicate". A hidden button on a
|
|
1130
|
+
record page header reporting "a list conditional predicate" sends its author to
|
|
1131
|
+
the list view to look for a control that was never there. Both messages this
|
|
1132
|
+
module emits (evaluation failure and the legacy-dialect deprecation) now read
|
|
1133
|
+
"conditional predicate".
|
|
1134
|
+
|
|
1135
|
+
**The row is the subject — on both dialect paths (objectui#3796).** The scope
|
|
1136
|
+
merge pinned `data` after the host-scope spread but not `record`, leaving
|
|
1137
|
+
`record` to each engine's own binding — and the two engines disagreed. The legacy
|
|
1138
|
+
evaluator re-pinned `record` to the row (row won); the CEL engine takes its
|
|
1139
|
+
`extra` bag over its `record` binding, so a host scope carrying a `record` key
|
|
1140
|
+
won there instead. One function, one predicate text, two subjects — selected by
|
|
1141
|
+
whether the string happens to contain `===` or `${…}`, the dialect-routing
|
|
1142
|
+
markers, which no author picks deliberately. `record` is now pinned after the
|
|
1143
|
+
spread exactly as `data` is, which fixes both paths at the merge site rather than
|
|
1144
|
+
relying on either engine's precedence.
|
|
1145
|
+
|
|
1146
|
+
No host injects a `record` key today — `ExpressionProvider` binds
|
|
1147
|
+
`current_user` / `user` / `ctx` / `os` / `app` / `data` / `features` — so nothing
|
|
1148
|
+
observable changes for existing apps; this closes the edge before a host adds one
|
|
1149
|
+
(`ctx.record` already exists, which is exactly how it would arrive). A row FIELD
|
|
1150
|
+
named `record` is likewise no longer able to become the row root; it stays
|
|
1151
|
+
addressable as `data.record`.
|
|
1152
|
+
|
|
1153
|
+
Shipped as a patch: no new exported symbol, one optional field added to an
|
|
1154
|
+
already-exported options interface, and no existing call signature changed.
|
|
1155
|
+
|
|
1156
|
+
- 523be48: `object-timeline` and `record:line_items` now apply the filter / sort / row cap they are given, so a named `dataSource.view` narrows them instead of contributing nothing
|
|
1157
|
+
|
|
1158
|
+
These were the two residual gaps in objectstack#7121's per-block coverage table
|
|
1159
|
+
(objectstack#7137). Both blocks are object-bound lists, both accepted the spec's
|
|
1160
|
+
per-element `dataSource` binding, and neither had a read site for `filter` or
|
|
1161
|
+
`sort` anywhere in its fetch:
|
|
1162
|
+
|
|
1163
|
+
- `object-timeline`'s entire query was
|
|
1164
|
+
`find(objectName, { options: { $top: 100 } })`.
|
|
1165
|
+
- `record:line_items`' was the parent FK plus a fixed `$top: 500`.
|
|
1166
|
+
|
|
1167
|
+
So `dataSource: { object, view: 'hot' }` resolved the view — a typo still reported
|
|
1168
|
+
a configuration error, it never degraded into an unfiltered query — and then
|
|
1169
|
+
dropped everything the view said. The rendered rows could be **wider than the view
|
|
1170
|
+
they named**, silently, which is exactly the class of mistake AI-authored metadata
|
|
1171
|
+
hides best: the page looks like it works. objectstack#7121 deliberately left the
|
|
1172
|
+
keys unmapped and recorded the gap rather than writing composed values onto schema
|
|
1173
|
+
keys nobody read; this closes it at the fetch instead.
|
|
1174
|
+
|
|
1175
|
+
What each block now reads:
|
|
1176
|
+
|
|
1177
|
+
- **`object-timeline`** — `$filter: schema.filter`,
|
|
1178
|
+
`$orderby: convertSortToQueryParams(schema.sort)`, and
|
|
1179
|
+
`$top: schema.limit ?? 100`, matching the form `object-gantt` / `object-map` /
|
|
1180
|
+
`object-calendar` already use. Its registry mapping gains
|
|
1181
|
+
`filter` / `sort` / `limit`; `columns` stays unmapped, because a timeline
|
|
1182
|
+
projects the fields its `timeline` config names.
|
|
1183
|
+
- **`record:line_items`** — the composed filter is **AND-combined** with the parent
|
|
1184
|
+
relationship condition through `mergeFilterNodes`, never substituted for it, the
|
|
1185
|
+
same way `record:related_list` composes its own since objectstack#7118: a
|
|
1186
|
+
line-items panel is always scoped to the record it sits on, so an _additional_
|
|
1187
|
+
criterion can only narrow this parent's children and can never surface another
|
|
1188
|
+
parent's rows. `sort` becomes the load order and `limit` the row cap (default
|
|
1189
|
+
500). `columns` stays unmapped — here they are `GridColumn[]` driving an editable
|
|
1190
|
+
grid, not a field-name projection, so a view's column list would be the wrong
|
|
1191
|
+
_shape_ rather than merely a wider answer.
|
|
1192
|
+
|
|
1193
|
+
**Behaviour change worth knowing about:** the timeline's default window is now a
|
|
1194
|
+
real cap. `{ options: { $top: 100 } }` nested the limit under a key that is not a
|
|
1195
|
+
`QueryParams` field and that no adapter in this repo reads (`convertQueryParams`
|
|
1196
|
+
maps `params.$top`), so the intended window never reached the wire and a timeline
|
|
1197
|
+
over a large object fetched whatever the server chose to return. It is now sent as
|
|
1198
|
+
`$top`, and authorable via `limit` or a view's `pagination.pageSize`.
|
|
1199
|
+
|
|
1200
|
+
`@object-ui/core` gains `convertSortToQueryParams`, the sort→`$orderby` lowering
|
|
1201
|
+
the three sibling blocks each inline privately. It is shared rather than copied
|
|
1202
|
+
twice more, and is slightly more faithful to the declared contract than those
|
|
1203
|
+
copies: a sort entry that omits `order` means ascending instead of being dropped
|
|
1204
|
+
(the string spelling `"amount"` already meant ascending in the same copies), and
|
|
1205
|
+
nothing orderable yields `undefined` rather than a truthy empty `{}`. Migrating
|
|
1206
|
+
the three existing copies onto it is objectstack#7148 and is not done here.
|
|
1207
|
+
|
|
1208
|
+
- Updated dependencies [d229dfa]
|
|
1209
|
+
- Updated dependencies [c2fd122]
|
|
1210
|
+
- Updated dependencies [48132f7]
|
|
1211
|
+
- Updated dependencies [7e5bb5d]
|
|
1212
|
+
- Updated dependencies [e6fdbdc]
|
|
1213
|
+
- Updated dependencies [7e2b7e9]
|
|
1214
|
+
- Updated dependencies [c1e1e6b]
|
|
1215
|
+
- @object-ui/types@17.4.0
|
|
1216
|
+
|
|
1217
|
+
## 17.3.0
|
|
1218
|
+
|
|
1219
|
+
### Minor Changes
|
|
1220
|
+
|
|
1221
|
+
- 5781fb1: `@object-ui/core` now ships the server-action dispatcher factory —
|
|
1222
|
+
`createServerActionHandler({ fetch, baseUrl, resolveObject, ... })` — so any
|
|
1223
|
+
consumer of the runner (standalone renderers, SDUI hosts, embedded usage) can
|
|
1224
|
+
run `action.body` script actions by registering the produced handler, instead
|
|
1225
|
+
of dead-ending on the built-in `executeScript`'s "must be executed server-side"
|
|
1226
|
+
error with no supported way to make it run (objectui#2904, the follow-up
|
|
1227
|
+
objectui#2896 deferred).
|
|
1228
|
+
|
|
1229
|
+
The factory is deliberately opinion-free about the three things core has no
|
|
1230
|
+
business deciding — auth (`fetch` is an injected authenticated wrapper), origin
|
|
1231
|
+
(`baseUrl` string or thunk; no bundler env convention), and fallback object
|
|
1232
|
+
scope (`resolveObject`) — and owns everything protocol-shaped, once:
|
|
1233
|
+
|
|
1234
|
+
- name-based action identity (ADR-0110 D1 — `target` is a binding expression,
|
|
1235
|
+
never an identity);
|
|
1236
|
+
- the record-id resolution dance, also exported as
|
|
1237
|
+
`resolveServerActionRecordId` (`_rowRecord`, `recordIdField`, toolbar
|
|
1238
|
+
selection fallback with its single/zero-select guards, aggregate
|
|
1239
|
+
`_selectedIds` bypass), replaceable wholesale via `resolveRecordId` for
|
|
1240
|
+
hosts with their own policy (record pages);
|
|
1241
|
+
- a re-entrancy guard per action+record;
|
|
1242
|
+
- the `/actions` response-envelope rule: `interpretActionResponse`,
|
|
1243
|
+
`readActionPayload` and `actionErrorDetail` moved from `@object-ui/app-shell`
|
|
1244
|
+
internals into core and are now public exports.
|
|
1245
|
+
|
|
1246
|
+
`@object-ui/app-shell`'s two hand-rolled copies of this POST —
|
|
1247
|
+
`useConsoleActionRuntime.serverActionHandler` and `RecordDetailView`'s — are
|
|
1248
|
+
collapsed into one console wrapper (`createConsoleServerActionHandler`) that
|
|
1249
|
+
layers the browser-only choreography (popup pre-open dance, zero-roundtrip
|
|
1250
|
+
`newTabUrl` fast path, `redirectUrl` convention) over the core factory. The
|
|
1251
|
+
copies had already drifted twice (objectstack#3913 — envelope; framework#3935 —
|
|
1252
|
+
identity, fixed in one copy only): RecordDetailView now also dispatches by
|
|
1253
|
+
declarative `name` instead of `target || name`, and no longer leaks the
|
|
1254
|
+
client-side `_rowRecord` stash to the server.
|
|
1255
|
+
|
|
1256
|
+
- f833d3a: Retire `validation` from the action-param contract — it was declared on both
|
|
1257
|
+
halves, read by neither, and rejected outright by the server (objectui#3201).
|
|
1258
|
+
|
|
1259
|
+
FROM: `validation?: string` was declared on the AUTHORING type
|
|
1260
|
+
(`@object-ui/types`' `ActionParam`) and on the RESOLVED type (`@object-ui/core`'s
|
|
1261
|
+
`ActionParamDef`). TO: it is declared on neither.
|
|
1262
|
+
|
|
1263
|
+
**Breaking for anyone who declared it — but it never did anything.** This is
|
|
1264
|
+
marked `minor`, not `major`, per the repo's version-alignment policy (objectui's
|
|
1265
|
+
major tracks `@objectstack`'s, so objectui's own breaking changes ship as `minor`
|
|
1266
|
+
with the breaking semantics spelled out here).
|
|
1267
|
+
|
|
1268
|
+
**Migration: delete it.** If you authored `validation: '...'` on an action param,
|
|
1269
|
+
it never took effect, and publishing that metadata to the server is a hard parse
|
|
1270
|
+
failure — so any metadata that reached production either never carried the key or
|
|
1271
|
+
never parsed. Removing it changes no runtime behaviour; it only moves the error
|
|
1272
|
+
from "silent no-op, then rejected at publish" to a `tsc` error at the keystroke.
|
|
1273
|
+
|
|
1274
|
+
Why it could not work as authored:
|
|
1275
|
+
|
|
1276
|
+
- `ActionParamSchema` in `@objectstack/spec/ui` is `.strict()` and does not list
|
|
1277
|
+
`validation`, so an authored key is a PARSE REJECTION on the server:
|
|
1278
|
+
`Unrecognized key(s) on this action param: \`validation\``. Meanwhile `tsc`
|
|
1279
|
+
against the public type accepted it — the type vouched for a key the platform
|
|
1280
|
+
itself refuses.
|
|
1281
|
+
- Nothing read it on the resolved side either: it was never a key of
|
|
1282
|
+
`resolveActionParams()`'s `RawActionParam`, the runtime field metadata a
|
|
1283
|
+
field-backed param inherits from carries no `validation` to source one from,
|
|
1284
|
+
and `paramToField()` never mapped it — so it could not reach the field widgets,
|
|
1285
|
+
whose rules `buildValidationRules()` builds from `required` / `minLength` /
|
|
1286
|
+
`maxLength` / `pattern`.
|
|
1287
|
+
|
|
1288
|
+
Removed rather than implemented, on ADR-0049 enforce-or-remove. Giving it meaning
|
|
1289
|
+
would mean first deciding what an "expression" is here (CEL? a formula? a regex?)
|
|
1290
|
+
and adding it to `@objectstack/spec`, which is where such a capability has to
|
|
1291
|
+
start — not accreted renderer-side around a key the contract does not have.
|
|
1292
|
+
|
|
1293
|
+
This also retires the last named exception in objectui#3174's drift guard
|
|
1294
|
+
(`packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts`), which carried
|
|
1295
|
+
`validation` as the one key `ActionParam` added on top of the spec's set. The
|
|
1296
|
+
rule it pins — **the authoring type declares exactly the spec's authorable
|
|
1297
|
+
keys** — is now literal: the guard asserts the local-only key set is empty, so
|
|
1298
|
+
any future addition fails the build instead of being waved through.
|
|
1299
|
+
|
|
1300
|
+
- d22ae31: Track `@objectstack/spec` 17.0.0-rc.2 (objectui#3235, #3208, #3287, #3264).
|
|
1301
|
+
|
|
1302
|
+
The pin moves from `^17.0.0-rc.1` to `^17.0.0-rc.2` across the workspace, and
|
|
1303
|
+
the sibling `@objectstack/*` packages (`client` / `core` / `formula` / `lint`)
|
|
1304
|
+
move with it — they pin `@objectstack/spec` **exactly**, so leaving them behind
|
|
1305
|
+
kept a second copy of the spec in the tree and would have had `@objectstack/lint`
|
|
1306
|
+
validating against rc.1 schemas that still accept keys rc.2 retires.
|
|
1307
|
+
|
|
1308
|
+
Breaking semantics, in FROM → TO form:
|
|
1309
|
+
|
|
1310
|
+
- **`app.homePageId` is retired — an app's landing page is now its first
|
|
1311
|
+
navigation item.** An app that pinned a landing page with `homePageId` will
|
|
1312
|
+
open on the first reachable navigation entry (by `order`) instead; the root
|
|
1313
|
+
landing still follows `isDefault`. To restore a specific landing page, reorder
|
|
1314
|
+
`navigation` so the intended entry comes first. Stored metadata is migrated by
|
|
1315
|
+
`os migrate meta --from 16`. The key is a hard error now, not a stripped one:
|
|
1316
|
+
the spec ships a tombstone that names the migration.
|
|
1317
|
+
Upstream retired it because of its SHAPE, not its usage — it was an ID
|
|
1318
|
+
cross-reference with no referential integrity, so a `homePageId` that pointed
|
|
1319
|
+
at nothing silently fell back to the first navigation item anyway
|
|
1320
|
+
(objectstack#4667, premise corrected in #4709). If the capability returns, it
|
|
1321
|
+
returns as a flag on the navigation item itself, which cannot dangle.
|
|
1322
|
+
- **`@object-ui/types`' `HttpMethod` now resolves to the spec's
|
|
1323
|
+
`HttpMethodType`.** Shape is verbatim identical — the same 5-value UI subset —
|
|
1324
|
+
and `@object-ui/types` still exports it as `HttpMethod`, so no consumer
|
|
1325
|
+
changes. The spec renamed its `./ui` export because `HttpMethod` named two
|
|
1326
|
+
different types depending on the import path (`./shared` / `./api` carry a
|
|
1327
|
+
7-value enum including `HEAD` / `OPTIONS`); objectui deliberately keeps the
|
|
1328
|
+
5-value one (objectstack#4691).
|
|
1329
|
+
- **`AppContextSelector.includeAll` / `placement` are gone.** Neither ever did
|
|
1330
|
+
anything in this renderer: context selectors are mandatory-scope, so no "All"
|
|
1331
|
+
row was ever rendered, and `placement: 'topbar'` put nothing in the topbar.
|
|
1332
|
+
Both carried schema defaults, which is why the liveness lint structurally
|
|
1333
|
+
could not flag them — removal was the only channel that reaches an author
|
|
1334
|
+
(framework#4509).
|
|
1335
|
+
- **`NavigationArea.visible` / `order` / `requiredPermissions` are gone.** An
|
|
1336
|
+
area is a layout grouping, not an access boundary. Gating moved down to the
|
|
1337
|
+
navigation ITEM, where `visible` and `requiredPermissions` are unchanged and
|
|
1338
|
+
still enforced. `AppSchemaRenderer`'s area switcher no longer hides an area, so
|
|
1339
|
+
an area whose items are all gated away renders as visible-but-empty rather
|
|
1340
|
+
than disappearing.
|
|
1341
|
+
- **`@object-ui/core` no longer exports `NotificationProtocol`**
|
|
1342
|
+
(`resolveNotificationConfig`, `specNotificationToToast`, `mapSeverityToVariant`,
|
|
1343
|
+
`mapPosition`, `ToastNotification`). It bridged `@objectstack/spec/ui`'s
|
|
1344
|
+
`Notification` / `NotificationConfig`, which objectstack#4610 deleted with no
|
|
1345
|
+
successor. Use `resolveNotificationConfig` from `@object-ui/react`
|
|
1346
|
+
(`NotificationContext`), which owns the live `NotificationSystemConfig` and is
|
|
1347
|
+
what every notification surface already read. Note that the spec's _other_
|
|
1348
|
+
`Notification` — `@objectstack/spec/api` — is the REST inbox row, a different
|
|
1349
|
+
contract, and is deliberately NOT aliased in as a replacement.
|
|
1350
|
+
- **The `email_template` client-side validator now uses
|
|
1351
|
+
`EmailTemplateDefinitionSchema`.** It was pointing at the removed
|
|
1352
|
+
`EmailTemplateSchema`, so authored templates were being checked against the
|
|
1353
|
+
wrong contract: the live one is keyed `name` + `locale` (not `id`) and splits
|
|
1354
|
+
the body into `bodyHtml` / `bodyText` (not `body` + `bodyType`)
|
|
1355
|
+
(objectstack#4616 / #4807).
|
|
1356
|
+
|
|
1357
|
+
Fixes that are not breaking, but were only found because rc.2 stopped being
|
|
1358
|
+
lenient — each had been passing vacuously:
|
|
1359
|
+
|
|
1360
|
+
- **`view` drafts are actually validated now.** The client validator named the
|
|
1361
|
+
aggregated container schema while this admin authors first-class `ViewItem`s,
|
|
1362
|
+
and the container used to strip `viewKind` / `config` in silence — so no view
|
|
1363
|
+
draft ever had one of its own keys checked. It now validates each shape
|
|
1364
|
+
against its own schema (objectui#3312).
|
|
1365
|
+
- **The console's worked examples were wrong**, and being stripped rather than
|
|
1366
|
+
refused: `view.list.object` (the container root already declares it),
|
|
1367
|
+
`job.concurrency` / `job.timeoutMs` (no such keys; the spelling is `timeout`,
|
|
1368
|
+
already in ms), `email_template.from` / `.to` (a template is not a send —
|
|
1369
|
+
the sender override is `fromOverride`, an object), and
|
|
1370
|
+
`datasource.capabilities` / `.healthCheck` (objectstack#4583 removed the
|
|
1371
|
+
former; the latter was never a datasource key). These are the drafts an
|
|
1372
|
+
author — or a model generating metadata — copies.
|
|
1373
|
+
- Action key inventory re-derived: `ActionSchema` gained the package-lock
|
|
1374
|
+
envelope (`_lock*` / `_package*` / `_provenance`), so a packaged action no
|
|
1375
|
+
longer reports them as unknown keys.
|
|
1376
|
+
- The schema-diff panel labels the new `default_mismatch` finding.
|
|
1377
|
+
- Test fixtures pinning the retired `managedBy: 'system'` bucket now use
|
|
1378
|
+
`engine-owned`. Protocol 17 split that value (objectstack#3355), so it
|
|
1379
|
+
resolved to the default-writable fallback and a batch of "stays locked"
|
|
1380
|
+
assertions had quietly stopped asserting anything.
|
|
1381
|
+
|
|
1382
|
+
### Patch Changes
|
|
1383
|
+
|
|
1384
|
+
- 18cd432: `ActionEngine.getActionsForLocation` now evaluates a `{ dialect: 'cel', source }` action `visible` predicate on the canonical `@objectstack/formula` engine instead of the legacy JS evaluator. The method used to unwrap the envelope into a `${source}` string before calling `evaluateCondition`, which only routes to the CEL engine while its argument is still an envelope — so the engine silently demoted every CEL predicate to the legacy path while every action renderer (`action-button`, `action-menu`, `action-bar`, … via `toPredicateInput`) ran the same predicate on CEL (#2661). Because `ExpressionInputSchema` normalizes even a bare authored string into a CEL envelope, that was the common case, and the two engines disagree: `null < null` faults in CEL (fail-closed hide) but is `false` in JS, and CEL-only builtins such as `today()` do not exist on the JS path at all — so one `visible:` predicate could hide a button in the renderer while the engine kept it (or the reverse). Normalization now goes through a shared `toPredicateInput` helper exported from `@object-ui/core`, pinned to the renderer-side twin by a parity suite. Bare strings, non-`cel` dialects, empty `source`, and boolean predicates all behave exactly as before (#3314).
|
|
1385
|
+
- d915c47: Relation fields (`lookup` / `master_detail` / `user` / `tree`) are now usable in action and conditional-formatting predicates: they bind as the stored foreign key on every surface, and the fields a predicate reads are included in the query projection (#3501).
|
|
1386
|
+
|
|
1387
|
+
Before this, one predicate over one relation field had four different fates, decided by things its author does not control. `$expand` **replaces** the id in place with the whole related record, and a view expands exactly the relations it shows as COLUMNS — so `record.owner == "U1"` was **true** where the column was absent, **false** where it was displayed, and a **fault** where the field was neither displayed nor projected (a list's `$select` was built from its columns alone, and CEL treats an absent key as a fault, not as null). A fault is fail-CLOSED on the row kebab and the selection bar and fail-OPEN on the lenient paths, so the same authoring mistake hid the button from everyone on one surface and showed it to everyone on the next, with nothing on screen to point at either. The server, meanwhile, only ever sees the id — so client and server could not agree, which is the one thing ADR-0036 / ADR-0058 exist to guarantee.
|
|
1388
|
+
|
|
1389
|
+
Two changes close it. `toPredicateRecord` (new, `@object-ui/core`) collapses expanded relation values back to their ids when a record is bound for evaluation — driven by the object's own field types, not by sniffing for an `id` key, so a `json` field that happens to carry one is untouched. It is threaded through `evalRowPredicate` / `resolveConditionalFormatting` (via a new `fields` option), `useRowPredicate`, `partitionBulkRows`, and both `page:header` evaluators, with the object schema supplied by `ObjectGrid` / `ListView` / `ObjectKanban` / the record context. Kanban card formatting is threaded the same way, so a rule cannot match on the grid view of a list and silently never match on its board. Display is unaffected — a detail-page title still renders the related record's name, and the schema-only `kanban-ui` entry point (which has no object schema to offer) keeps using the payload verbatim. `collectPredicateFieldRefs` / `listViewPredicates` (new) harvest the `record.x` / `data.x` references out of a view's conditional formatting, row-action defs, bulk-action defs, promoted object actions and `userActions` overrides, and add them to `$select` — intersected with the object's declared fields plus the platform columns every object carries (`isProjectableField`), because an unknown key is not ignored by every backend. No `$expand` is added: a predicate wants the foreign key, which is what an unexpanded relation already is.
|
|
1390
|
+
|
|
1391
|
+
- 509104a: Fix matrix report cells showing another bucket's numbers when dimension values run together.
|
|
1392
|
+
|
|
1393
|
+
The cross-tab in `DatasetReportRenderer` built its bucket ids by joining dimension values with the EMPTY string, so adjacent values had no boundary at all: `"x"` + `"yz"` and `"xy"` + `"z"` were the same bucket on both axes, and the later row silently overwrote the earlier one. Its cell key then joined the two bucket ids with a plain space, while dimension values contain spaces constantly ("New York", "In Progress"), so `"New"` × `"York Q1"` and `"New York"` × `"Q1"` also met in one key. A merged bucket showed a different row's measure, the overwritten row's value was unreachable, the per-row and per-column subtotals matched the wrong header, and drill-through followed the same wrong index into another record's list — none of it with an error.
|
|
1394
|
+
|
|
1395
|
+
Bucket ids and cell keys are now encoded with `JSON.stringify`, which carries the boundary in its own quoting rather than in a character the data is assumed never to contain. All four lookups in the renderer (row headers, column headers, row subtotals, column subtotals) share the one encoder, so they agree by construction.
|
|
1396
|
+
|
|
1397
|
+
The encoders moved to `@object-ui/core` as `pivotBucketId` / `pivotCellKey` and are now shared with the dashboard `DatasetWidget`, which carried the same defect and fixed it separately: two packages each hand-rolling the same key is why one fix left the other broken. The dashboard keeps its existing exports and behaviour.
|
|
1398
|
+
|
|
1399
|
+
- a4cff5b: Conditional-rule predicates that fail to evaluate are no longer silent
|
|
1400
|
+
(objectstack#5149, appeal 2). `evalFieldPredicate` — the canonical funnel for
|
|
1401
|
+
`visibleWhen` / `readonlyWhen` / `requiredWhen`, view-level `visibleOn`, legacy
|
|
1402
|
+
`condition`, per-option `visibleWhen`, screen-field predicates and list
|
|
1403
|
+
conditional formatting — now logs **one `console.warn` per predicate text**
|
|
1404
|
+
when evaluation fails (parse error, unbound identifier, engine fault), carrying
|
|
1405
|
+
the predicate source, the engine's failure reason, and the field/rule locator
|
|
1406
|
+
the call site provides. Renderer call sites thread that locator
|
|
1407
|
+
(`visibleWhen of field 'amount'`), so a broken predicate identifies itself in
|
|
1408
|
+
the browser console instead of being indistinguishable from an absent one.
|
|
1409
|
+
|
|
1410
|
+
Verdicts are unchanged: evaluation still fails open to the caller's safe
|
|
1411
|
+
default (flipping that default is objectstack#5149 appeal 1, tracked
|
|
1412
|
+
separately). Fault-probing callers (`evalRowPredicate`'s fail-closed path,
|
|
1413
|
+
`ExpressionEvaluator`'s `throwOnError`) opt out via the new
|
|
1414
|
+
`diagnostic.warn: false` and keep their own single diagnostic, so no broken
|
|
1415
|
+
predicate ever warns twice.
|
|
1416
|
+
|
|
1417
|
+
- 2a9513d: `toFilterNode` now lowers a spec `ViewFilterRule[]` into ObjectQL AST nodes instead of returning the array verbatim, so a saved view's stored filter reaches `$filter` as something the server accepts. It previously did not: `ListViewSchema.filter` / `ViewTab.filter` are declared `z.array(ViewFilterRuleSchema)`, and the whole read path — `ObjectView` → `ListViewSchema.filter` → `buildEffectiveFilter` → `mergeFilterNodes` → `toFilterNode` — carried those rule objects untouched into the query. `isFilterAST` is `false` for an array of objects, so the data API answered `400 INVALID_FILTER` and the list rendered no rows at all. Measured against a real backend on the showcase's shipped `showcase_task.in_progress` view: `$filter=[{"field":"status","operator":"equals","value":"in_progress"}]` returned `400`, while the lowered `[["status","equals","in_progress"]]` returned its 2 rows. Every saved view carrying a filter was affected, on both producers that share this sink — `plugin-list`'s `buildEffectiveFilter` (the grid and its export) and `plugin-view`'s `ObjectView` (calendar / kanban / gallery / timeline).
|
|
1418
|
+
|
|
1419
|
+
Operators are canonicalised through the spec's own `normalizeFilterOperator`, the same exit the write side (`viewFilterFold`) uses, so the two directions cannot drift into two dialects; no second operator table is introduced. An operator the spec does not know is passed through verbatim so the server still refuses it loudly rather than having a misspelling coerced into a valid filter. AST nodes and MongoDB-style object filters are unaffected, mixed arrays (a view's rules concatenated with `?filter[<field>]=<value>` URL triples) fold element-wise with the triples untouched, and a rule with a blank `field` is deliberately left unlowered — `["", op, value]` passes `isFilterAST` and returns an empty list, whereas the unlowered rule keeps the loud `400`.
|
|
1420
|
+
|
|
1421
|
+
- Updated dependencies [d915c47]
|
|
1422
|
+
- Updated dependencies [9e9e9a9]
|
|
1423
|
+
- Updated dependencies [23018cc]
|
|
1424
|
+
- Updated dependencies [f44d872]
|
|
1425
|
+
- Updated dependencies [f833d3a]
|
|
1426
|
+
- Updated dependencies [d22ae31]
|
|
1427
|
+
- @object-ui/types@17.3.0
|
|
1428
|
+
|
|
3
1429
|
## 17.2.0
|
|
4
1430
|
|
|
5
1431
|
### Minor Changes
|
|
@@ -2429,6 +3855,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2429
3855
|
project `health`) painted its categories from the generic `--chart-1..5`
|
|
2430
3856
|
palette — the same gap the chart view (`object-chart`) had before #1932. It now
|
|
2431
3857
|
resolves the dimension field's option colors (using the dataset's base `object`
|
|
3858
|
+
|
|
2432
3859
|
- dimension→field map the query already returns) and threads them to the
|
|
2433
3860
|
renderer as a per-category `categoryColors` map, so health green/red/yellow
|
|
2434
3861
|
paints semantically.
|
|
@@ -2671,6 +4098,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2671
4098
|
- 991b62d: Add `compareTo` field to dashboard widgets for period-over-period
|
|
2672
4099
|
comparison. Supports `'previousPeriod'`, `'previousYear'`, and
|
|
2673
4100
|
`{ offset: '7d' | '4w' | '1M' | '1y' }`.
|
|
4101
|
+
|
|
2674
4102
|
- **Metric / gauge widgets** now compute a delta percentage when `compareTo`
|
|
2675
4103
|
is set and surface it as a derived `trend` (auto-labelled via
|
|
2676
4104
|
`dashboard.trend.vsLast*` i18n keys sniffed from the filter macros).
|
|
@@ -2773,6 +4201,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2773
4201
|
|
|
2774
4202
|
Extract the bulk-vs-per-row decision into a reusable
|
|
2775
4203
|
`executeBulkBatch(input, ops)` helper in `@object-ui/core`:
|
|
4204
|
+
|
|
2776
4205
|
- Single decision tree shared by both update and delete fast paths.
|
|
2777
4206
|
- Bulk success → no per-row pass.
|
|
2778
4207
|
- Bulk partial-count → aggregate batch error.
|
|
@@ -2817,6 +4246,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2817
4246
|
multi-user editing scenarios.
|
|
2818
4247
|
|
|
2819
4248
|
**`@object-ui/data-objectstack`**
|
|
4249
|
+
|
|
2820
4250
|
- Exports `ConcurrentUpdateError` (carries `currentVersion` and
|
|
2821
4251
|
`currentRecord`) and `isConcurrentUpdateError()` type guard.
|
|
2822
4252
|
- `update()` / `delete()` accept `opts.ifMatch` and forward it via the
|
|
@@ -2829,6 +4259,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2829
4259
|
from conflicts without parsing the wire format.
|
|
2830
4260
|
|
|
2831
4261
|
**`@object-ui/core`**
|
|
4262
|
+
|
|
2832
4263
|
- `ApiDataSource.update()` and `.delete()` accept `opts.ifMatch` and emit
|
|
2833
4264
|
the `If-Match` HTTP header.
|
|
2834
4265
|
|
|
@@ -2964,6 +4395,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2964
4395
|
detail view (`TypeError: titleFormat.replace is not a function`) and printed
|
|
2965
4396
|
`Failed to evaluate expression: ${[object Object]}` for every action visibility
|
|
2966
4397
|
predicate.
|
|
4398
|
+
|
|
2967
4399
|
- `@object-ui/core`: `ExpressionEvaluator.evaluate` / `evaluateCondition` now
|
|
2968
4400
|
unwrap Expression envelopes transparently.
|
|
2969
4401
|
- `@object-ui/react`: new `toPredicateInput()` helper to safely normalize
|
|
@@ -2978,6 +4410,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2978
4410
|
the new `{ source }` envelope.
|
|
2979
4411
|
|
|
2980
4412
|
All changes are backward-compatible — legacy bare strings continue to work.
|
|
4413
|
+
|
|
2981
4414
|
- @object-ui/types@4.0.7
|
|
2982
4415
|
|
|
2983
4416
|
## 4.0.6
|
|
@@ -3156,6 +4589,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
3156
4589
|
- New plugin-object and ObjectQL SDK updates
|
|
3157
4590
|
|
|
3158
4591
|
**Added:**
|
|
4592
|
+
|
|
3159
4593
|
- New Plugin: @object-ui/plugin-object - ObjectQL plugin for automatic table and form generation
|
|
3160
4594
|
- ObjectTable: Auto-generates tables from ObjectQL object schemas
|
|
3161
4595
|
- ObjectForm: Auto-generates forms from ObjectQL object schemas with create/edit/view modes
|
|
@@ -3164,6 +4598,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
3164
4598
|
- ObjectQL Integration: Enhanced ObjectQLDataSource with getObjectSchema() method using MetadataApiClient
|
|
3165
4599
|
|
|
3166
4600
|
**Changed:**
|
|
4601
|
+
|
|
3167
4602
|
- Updated @objectql/sdk from ^1.8.3 to ^1.9.1
|
|
3168
4603
|
- Updated @objectql/types from ^1.8.3 to ^1.9.1
|
|
3169
4604
|
|
|
@@ -3177,6 +4612,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
3177
4612
|
- Patch release: Add automated changeset workflow and CI/CD improvements
|
|
3178
4613
|
|
|
3179
4614
|
This release includes infrastructure improvements:
|
|
4615
|
+
|
|
3180
4616
|
- Added changeset-based version management
|
|
3181
4617
|
- Enhanced CI/CD workflows with GitHub Actions
|
|
3182
4618
|
- Improved documentation for contributing and releasing
|