@object-ui/core 17.1.0 → 17.3.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 +738 -0
- package/README.md +29 -0
- package/dist/actions/ActionEngine.d.ts +22 -7
- package/dist/actions/ActionEngine.js +40 -22
- package/dist/actions/ActionRunner.d.ts +133 -13
- package/dist/actions/ActionRunner.js +27 -4
- package/dist/actions/actionErrorDetail.d.ts +26 -0
- package/dist/actions/actionErrorDetail.js +37 -0
- package/dist/actions/actionKeys.d.ts +29 -12
- package/dist/actions/actionKeys.js +61 -10
- 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/evaluator/ExpressionEvaluator.js +4 -2
- package/dist/evaluator/fieldRules.d.ts +26 -2
- package/dist/evaluator/fieldRules.js +63 -8
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +13 -1
- package/dist/evaluator/listConditional.js +0 -0
- package/dist/evaluator/optionRules.js +5 -1
- package/dist/evaluator/predicateInput.d.ts +84 -0
- package/dist/evaluator/predicateInput.js +72 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +12 -0
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +8 -1
- package/dist/query/query-ast.d.ts +6 -6
- package/dist/query/query-ast.js +2 -2
- package/dist/registry/PluginSystem.d.ts +15 -5
- package/dist/registry/PluginSystem.js +1 -1
- package/dist/registry/WidgetRegistry.d.ts +7 -7
- package/dist/registry/WidgetRegistry.js +1 -1
- package/dist/styling/scoped-styles.d.ts +14 -8
- package/dist/utils/chart-series.d.ts +14 -2
- package/dist/utils/dashboard-filters.d.ts +9 -0
- package/dist/utils/dashboard-filters.js +99 -4
- package/dist/utils/dataset-format.d.ts +18 -1
- package/dist/utils/dataset-format.js +10 -2
- package/dist/utils/dataset-pivot.d.ts +57 -0
- package/dist/utils/dataset-pivot.js +57 -0
- package/dist/utils/filter-converter.d.ts +39 -11
- package/dist/utils/filter-converter.js +87 -7
- package/dist/utils/filter-tokens.d.ts +13 -6
- package/dist/utils/filter-tokens.js +13 -6
- package/dist/utils/freeze-schema.d.ts +14 -6
- package/dist/utils/freeze-schema.js +14 -6
- package/dist/utils/managedBy.d.ts +78 -50
- package/dist/utils/managedBy.js +75 -52
- package/dist/utils/normalize-list-view.d.ts +12 -2
- 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/validation/index.d.ts +1 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/required-presence.d.ts +31 -0
- package/dist/validation/required-presence.js +36 -0
- package/dist/validation/schema-validator.d.ts +24 -8
- package/package.json +4 -4
- package/dist/protocols/NotificationProtocol.d.ts +0 -71
- package/dist/protocols/NotificationProtocol.js +0 -89
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,743 @@
|
|
|
1
1
|
# @object-ui/core
|
|
2
2
|
|
|
3
|
+
## 17.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5781fb1: `@object-ui/core` now ships the server-action dispatcher factory —
|
|
8
|
+
`createServerActionHandler({ fetch, baseUrl, resolveObject, ... })` — so any
|
|
9
|
+
consumer of the runner (standalone renderers, SDUI hosts, embedded usage) can
|
|
10
|
+
run `action.body` script actions by registering the produced handler, instead
|
|
11
|
+
of dead-ending on the built-in `executeScript`'s "must be executed server-side"
|
|
12
|
+
error with no supported way to make it run (objectui#2904, the follow-up
|
|
13
|
+
objectui#2896 deferred).
|
|
14
|
+
|
|
15
|
+
The factory is deliberately opinion-free about the three things core has no
|
|
16
|
+
business deciding — auth (`fetch` is an injected authenticated wrapper), origin
|
|
17
|
+
(`baseUrl` string or thunk; no bundler env convention), and fallback object
|
|
18
|
+
scope (`resolveObject`) — and owns everything protocol-shaped, once:
|
|
19
|
+
|
|
20
|
+
- name-based action identity (ADR-0110 D1 — `target` is a binding expression,
|
|
21
|
+
never an identity);
|
|
22
|
+
- the record-id resolution dance, also exported as
|
|
23
|
+
`resolveServerActionRecordId` (`_rowRecord`, `recordIdField`, toolbar
|
|
24
|
+
selection fallback with its single/zero-select guards, aggregate
|
|
25
|
+
`_selectedIds` bypass), replaceable wholesale via `resolveRecordId` for
|
|
26
|
+
hosts with their own policy (record pages);
|
|
27
|
+
- a re-entrancy guard per action+record;
|
|
28
|
+
- the `/actions` response-envelope rule: `interpretActionResponse`,
|
|
29
|
+
`readActionPayload` and `actionErrorDetail` moved from `@object-ui/app-shell`
|
|
30
|
+
internals into core and are now public exports.
|
|
31
|
+
|
|
32
|
+
`@object-ui/app-shell`'s two hand-rolled copies of this POST —
|
|
33
|
+
`useConsoleActionRuntime.serverActionHandler` and `RecordDetailView`'s — are
|
|
34
|
+
collapsed into one console wrapper (`createConsoleServerActionHandler`) that
|
|
35
|
+
layers the browser-only choreography (popup pre-open dance, zero-roundtrip
|
|
36
|
+
`newTabUrl` fast path, `redirectUrl` convention) over the core factory. The
|
|
37
|
+
copies had already drifted twice (objectstack#3913 — envelope; framework#3935 —
|
|
38
|
+
identity, fixed in one copy only): RecordDetailView now also dispatches by
|
|
39
|
+
declarative `name` instead of `target || name`, and no longer leaks the
|
|
40
|
+
client-side `_rowRecord` stash to the server.
|
|
41
|
+
|
|
42
|
+
- f833d3a: Retire `validation` from the action-param contract — it was declared on both
|
|
43
|
+
halves, read by neither, and rejected outright by the server (objectui#3201).
|
|
44
|
+
|
|
45
|
+
FROM: `validation?: string` was declared on the AUTHORING type
|
|
46
|
+
(`@object-ui/types`' `ActionParam`) and on the RESOLVED type (`@object-ui/core`'s
|
|
47
|
+
`ActionParamDef`). TO: it is declared on neither.
|
|
48
|
+
|
|
49
|
+
**Breaking for anyone who declared it — but it never did anything.** This is
|
|
50
|
+
marked `minor`, not `major`, per the repo's version-alignment policy (objectui's
|
|
51
|
+
major tracks `@objectstack`'s, so objectui's own breaking changes ship as `minor`
|
|
52
|
+
with the breaking semantics spelled out here).
|
|
53
|
+
|
|
54
|
+
**Migration: delete it.** If you authored `validation: '...'` on an action param,
|
|
55
|
+
it never took effect, and publishing that metadata to the server is a hard parse
|
|
56
|
+
failure — so any metadata that reached production either never carried the key or
|
|
57
|
+
never parsed. Removing it changes no runtime behaviour; it only moves the error
|
|
58
|
+
from "silent no-op, then rejected at publish" to a `tsc` error at the keystroke.
|
|
59
|
+
|
|
60
|
+
Why it could not work as authored:
|
|
61
|
+
|
|
62
|
+
- `ActionParamSchema` in `@objectstack/spec/ui` is `.strict()` and does not list
|
|
63
|
+
`validation`, so an authored key is a PARSE REJECTION on the server:
|
|
64
|
+
`Unrecognized key(s) on this action param: \`validation\``. Meanwhile `tsc`
|
|
65
|
+
against the public type accepted it — the type vouched for a key the platform
|
|
66
|
+
itself refuses.
|
|
67
|
+
- Nothing read it on the resolved side either: it was never a key of
|
|
68
|
+
`resolveActionParams()`'s `RawActionParam`, the runtime field metadata a
|
|
69
|
+
field-backed param inherits from carries no `validation` to source one from,
|
|
70
|
+
and `paramToField()` never mapped it — so it could not reach the field widgets,
|
|
71
|
+
whose rules `buildValidationRules()` builds from `required` / `minLength` /
|
|
72
|
+
`maxLength` / `pattern`.
|
|
73
|
+
|
|
74
|
+
Removed rather than implemented, on ADR-0049 enforce-or-remove. Giving it meaning
|
|
75
|
+
would mean first deciding what an "expression" is here (CEL? a formula? a regex?)
|
|
76
|
+
and adding it to `@objectstack/spec`, which is where such a capability has to
|
|
77
|
+
start — not accreted renderer-side around a key the contract does not have.
|
|
78
|
+
|
|
79
|
+
This also retires the last named exception in objectui#3174's drift guard
|
|
80
|
+
(`packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts`), which carried
|
|
81
|
+
`validation` as the one key `ActionParam` added on top of the spec's set. The
|
|
82
|
+
rule it pins — **the authoring type declares exactly the spec's authorable
|
|
83
|
+
keys** — is now literal: the guard asserts the local-only key set is empty, so
|
|
84
|
+
any future addition fails the build instead of being waved through.
|
|
85
|
+
|
|
86
|
+
- d22ae31: Track `@objectstack/spec` 17.0.0-rc.2 (objectui#3235, #3208, #3287, #3264).
|
|
87
|
+
|
|
88
|
+
The pin moves from `^17.0.0-rc.1` to `^17.0.0-rc.2` across the workspace, and
|
|
89
|
+
the sibling `@objectstack/*` packages (`client` / `core` / `formula` / `lint`)
|
|
90
|
+
move with it — they pin `@objectstack/spec` **exactly**, so leaving them behind
|
|
91
|
+
kept a second copy of the spec in the tree and would have had `@objectstack/lint`
|
|
92
|
+
validating against rc.1 schemas that still accept keys rc.2 retires.
|
|
93
|
+
|
|
94
|
+
Breaking semantics, in FROM → TO form:
|
|
95
|
+
|
|
96
|
+
- **`app.homePageId` is retired — an app's landing page is now its first
|
|
97
|
+
navigation item.** An app that pinned a landing page with `homePageId` will
|
|
98
|
+
open on the first reachable navigation entry (by `order`) instead; the root
|
|
99
|
+
landing still follows `isDefault`. To restore a specific landing page, reorder
|
|
100
|
+
`navigation` so the intended entry comes first. Stored metadata is migrated by
|
|
101
|
+
`os migrate meta --from 16`. The key is a hard error now, not a stripped one:
|
|
102
|
+
the spec ships a tombstone that names the migration.
|
|
103
|
+
Upstream retired it because of its SHAPE, not its usage — it was an ID
|
|
104
|
+
cross-reference with no referential integrity, so a `homePageId` that pointed
|
|
105
|
+
at nothing silently fell back to the first navigation item anyway
|
|
106
|
+
(objectstack#4667, premise corrected in #4709). If the capability returns, it
|
|
107
|
+
returns as a flag on the navigation item itself, which cannot dangle.
|
|
108
|
+
- **`@object-ui/types`' `HttpMethod` now resolves to the spec's
|
|
109
|
+
`HttpMethodType`.** Shape is verbatim identical — the same 5-value UI subset —
|
|
110
|
+
and `@object-ui/types` still exports it as `HttpMethod`, so no consumer
|
|
111
|
+
changes. The spec renamed its `./ui` export because `HttpMethod` named two
|
|
112
|
+
different types depending on the import path (`./shared` / `./api` carry a
|
|
113
|
+
7-value enum including `HEAD` / `OPTIONS`); objectui deliberately keeps the
|
|
114
|
+
5-value one (objectstack#4691).
|
|
115
|
+
- **`AppContextSelector.includeAll` / `placement` are gone.** Neither ever did
|
|
116
|
+
anything in this renderer: context selectors are mandatory-scope, so no "All"
|
|
117
|
+
row was ever rendered, and `placement: 'topbar'` put nothing in the topbar.
|
|
118
|
+
Both carried schema defaults, which is why the liveness lint structurally
|
|
119
|
+
could not flag them — removal was the only channel that reaches an author
|
|
120
|
+
(framework#4509).
|
|
121
|
+
- **`NavigationArea.visible` / `order` / `requiredPermissions` are gone.** An
|
|
122
|
+
area is a layout grouping, not an access boundary. Gating moved down to the
|
|
123
|
+
navigation ITEM, where `visible` and `requiredPermissions` are unchanged and
|
|
124
|
+
still enforced. `AppSchemaRenderer`'s area switcher no longer hides an area, so
|
|
125
|
+
an area whose items are all gated away renders as visible-but-empty rather
|
|
126
|
+
than disappearing.
|
|
127
|
+
- **`@object-ui/core` no longer exports `NotificationProtocol`**
|
|
128
|
+
(`resolveNotificationConfig`, `specNotificationToToast`, `mapSeverityToVariant`,
|
|
129
|
+
`mapPosition`, `ToastNotification`). It bridged `@objectstack/spec/ui`'s
|
|
130
|
+
`Notification` / `NotificationConfig`, which objectstack#4610 deleted with no
|
|
131
|
+
successor. Use `resolveNotificationConfig` from `@object-ui/react`
|
|
132
|
+
(`NotificationContext`), which owns the live `NotificationSystemConfig` and is
|
|
133
|
+
what every notification surface already read. Note that the spec's _other_
|
|
134
|
+
`Notification` — `@objectstack/spec/api` — is the REST inbox row, a different
|
|
135
|
+
contract, and is deliberately NOT aliased in as a replacement.
|
|
136
|
+
- **The `email_template` client-side validator now uses
|
|
137
|
+
`EmailTemplateDefinitionSchema`.** It was pointing at the removed
|
|
138
|
+
`EmailTemplateSchema`, so authored templates were being checked against the
|
|
139
|
+
wrong contract: the live one is keyed `name` + `locale` (not `id`) and splits
|
|
140
|
+
the body into `bodyHtml` / `bodyText` (not `body` + `bodyType`)
|
|
141
|
+
(objectstack#4616 / #4807).
|
|
142
|
+
|
|
143
|
+
Fixes that are not breaking, but were only found because rc.2 stopped being
|
|
144
|
+
lenient — each had been passing vacuously:
|
|
145
|
+
|
|
146
|
+
- **`view` drafts are actually validated now.** The client validator named the
|
|
147
|
+
aggregated container schema while this admin authors first-class `ViewItem`s,
|
|
148
|
+
and the container used to strip `viewKind` / `config` in silence — so no view
|
|
149
|
+
draft ever had one of its own keys checked. It now validates each shape
|
|
150
|
+
against its own schema (objectui#3312).
|
|
151
|
+
- **The console's worked examples were wrong**, and being stripped rather than
|
|
152
|
+
refused: `view.list.object` (the container root already declares it),
|
|
153
|
+
`job.concurrency` / `job.timeoutMs` (no such keys; the spelling is `timeout`,
|
|
154
|
+
already in ms), `email_template.from` / `.to` (a template is not a send —
|
|
155
|
+
the sender override is `fromOverride`, an object), and
|
|
156
|
+
`datasource.capabilities` / `.healthCheck` (objectstack#4583 removed the
|
|
157
|
+
former; the latter was never a datasource key). These are the drafts an
|
|
158
|
+
author — or a model generating metadata — copies.
|
|
159
|
+
- Action key inventory re-derived: `ActionSchema` gained the package-lock
|
|
160
|
+
envelope (`_lock*` / `_package*` / `_provenance`), so a packaged action no
|
|
161
|
+
longer reports them as unknown keys.
|
|
162
|
+
- The schema-diff panel labels the new `default_mismatch` finding.
|
|
163
|
+
- Test fixtures pinning the retired `managedBy: 'system'` bucket now use
|
|
164
|
+
`engine-owned`. Protocol 17 split that value (objectstack#3355), so it
|
|
165
|
+
resolved to the default-writable fallback and a batch of "stays locked"
|
|
166
|
+
assertions had quietly stopped asserting anything.
|
|
167
|
+
|
|
168
|
+
### Patch Changes
|
|
169
|
+
|
|
170
|
+
- 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).
|
|
171
|
+
- 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).
|
|
172
|
+
|
|
173
|
+
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.
|
|
174
|
+
|
|
175
|
+
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.
|
|
176
|
+
|
|
177
|
+
- 509104a: Fix matrix report cells showing another bucket's numbers when dimension values run together.
|
|
178
|
+
|
|
179
|
+
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.
|
|
180
|
+
|
|
181
|
+
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.
|
|
182
|
+
|
|
183
|
+
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.
|
|
184
|
+
|
|
185
|
+
- a4cff5b: Conditional-rule predicates that fail to evaluate are no longer silent
|
|
186
|
+
(objectstack#5149, appeal 2). `evalFieldPredicate` — the canonical funnel for
|
|
187
|
+
`visibleWhen` / `readonlyWhen` / `requiredWhen`, view-level `visibleOn`, legacy
|
|
188
|
+
`condition`, per-option `visibleWhen`, screen-field predicates and list
|
|
189
|
+
conditional formatting — now logs **one `console.warn` per predicate text**
|
|
190
|
+
when evaluation fails (parse error, unbound identifier, engine fault), carrying
|
|
191
|
+
the predicate source, the engine's failure reason, and the field/rule locator
|
|
192
|
+
the call site provides. Renderer call sites thread that locator
|
|
193
|
+
(`visibleWhen of field 'amount'`), so a broken predicate identifies itself in
|
|
194
|
+
the browser console instead of being indistinguishable from an absent one.
|
|
195
|
+
|
|
196
|
+
Verdicts are unchanged: evaluation still fails open to the caller's safe
|
|
197
|
+
default (flipping that default is objectstack#5149 appeal 1, tracked
|
|
198
|
+
separately). Fault-probing callers (`evalRowPredicate`'s fail-closed path,
|
|
199
|
+
`ExpressionEvaluator`'s `throwOnError`) opt out via the new
|
|
200
|
+
`diagnostic.warn: false` and keep their own single diagnostic, so no broken
|
|
201
|
+
predicate ever warns twice.
|
|
202
|
+
|
|
203
|
+
- 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).
|
|
204
|
+
|
|
205
|
+
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`.
|
|
206
|
+
|
|
207
|
+
- Updated dependencies [d915c47]
|
|
208
|
+
- Updated dependencies [9e9e9a9]
|
|
209
|
+
- Updated dependencies [23018cc]
|
|
210
|
+
- Updated dependencies [f44d872]
|
|
211
|
+
- Updated dependencies [f833d3a]
|
|
212
|
+
- Updated dependencies [d22ae31]
|
|
213
|
+
- @object-ui/types@17.3.0
|
|
214
|
+
|
|
215
|
+
## 17.2.0
|
|
216
|
+
|
|
217
|
+
### Minor Changes
|
|
218
|
+
|
|
219
|
+
- bca45cc: Declare the 18 spec-owned action keys `ActionDef` had been absorbing silently.
|
|
220
|
+
|
|
221
|
+
`ActionDef` ends with `[key: string]: any`, so it accepted any key of any type —
|
|
222
|
+
a typo (`targt`) and a retired spec key (`execute`) both type-checked, then the
|
|
223
|
+
runner silently bound no handler (the #2169 "Mark Done does nothing" shape).
|
|
224
|
+
Step 1 (objectstack#4075) made that audible with a dev-mode warning. This is
|
|
225
|
+
step 2: the keys the warning identified as legitimate are now real fields.
|
|
226
|
+
|
|
227
|
+
- **18 keys promoted to explicit optional fields** — `ai`, `aria`, `bodyExtra`,
|
|
228
|
+
`bodyShape`, `bulkEnabled`, `component`, `icon`, `locations`, `mode`,
|
|
229
|
+
`objectName`, `order`, `recordIdField`, `recordIdParam`, `requiredPermissions`,
|
|
230
|
+
`requiresFeature`, `shortcut`, `variant`, `visible`. Every type is **derived**
|
|
231
|
+
from `@objectstack/spec`'s `ActionInput` (`SpecActionInput['locations']`, …),
|
|
232
|
+
never hand-copied: a hand-written duplicate of a spec shape is a second
|
|
233
|
+
contract that drifts, which is the failure this issue is about. Wrong-typed
|
|
234
|
+
values are now compile errors — `order: 'first'`, `variant: 'chartreuse'`,
|
|
235
|
+
`locations: ['nope']` — where before they were absorbed silently.
|
|
236
|
+
- **Derived from `z.input`, not `z.infer`.** `ActionSchema` is a `ZodPipe` whose
|
|
237
|
+
transform narrows `visible` from `string | { dialect, source }` to the
|
|
238
|
+
envelope alone. This runner consumes authored/stored rows, which are
|
|
239
|
+
rehydrated unparsed, so it sees the input shape; deriving from the inferred
|
|
240
|
+
`Action` would have rejected the raw-string predicate `ActionEngine`
|
|
241
|
+
explicitly supports.
|
|
242
|
+
- **Three `as any` casts deleted** in `ActionEngine` — `visible` and
|
|
243
|
+
`requiredPermissions` at the location filter, `locations` at registration.
|
|
244
|
+
They existed only because the fields were undeclared.
|
|
245
|
+
- **Four objectui-dialect keys marked `@deprecated`** with the spec spelling to
|
|
246
|
+
use instead — `actionType` (→ `type`), `api` and `endpoint` (→ `target`;
|
|
247
|
+
`executeAPI` already resolves `api || endpoint || target`), and `navigate`
|
|
248
|
+
(→ flat `target` / `openIn`). Only these four: the remaining dialect keys are
|
|
249
|
+
runner mechanics (chaining, toasts, post-execution reload/close) with no spec
|
|
250
|
+
counterpart, and pointing them at a spelling that does not exist would be
|
|
251
|
+
worse than leaving them declared.
|
|
252
|
+
|
|
253
|
+
**Breaking edge, deliberate.** `shortcut` and `bulkEnabled` were retired by
|
|
254
|
+
`@objectstack/spec` 17 as `retiredKey()` tombstones (`z.never()`), so authoring
|
|
255
|
+
either is already a hard parse rejection. Deriving their types rather than
|
|
256
|
+
hand-writing them turns that runtime rejection into a **compile error**: code
|
|
257
|
+
that assigned `shortcut: 'ctrl+k'` to an `ActionDef` compiled before and does
|
|
258
|
+
not now. Such metadata was already refused by the platform — this only moves the
|
|
259
|
+
failure to where it can be fixed. A host may still pass either explicitly via
|
|
260
|
+
`ActionEngine.registerAction(action, { shortcut, bulkEnabled })`; only authored
|
|
261
|
+
metadata stopped carrying them. `bulkEnabled`'s replacement is the list view's
|
|
262
|
+
`bulkActions` / `bulkActionDefs`; `shortcut` has none.
|
|
263
|
+
|
|
264
|
+
The index signature **stays** — removing it is step 3, and the inverted pin
|
|
265
|
+
asserting it is still present remains the issue's own completion check.
|
|
266
|
+
|
|
267
|
+
- 4bf612c: Aggregate single-call mode for bulk actions: `execution: 'aggregate'` (objectui#3139).
|
|
268
|
+
|
|
269
|
+
A `bulkActionDefs` entry with `operation: 'custom'` used to have exactly one
|
|
270
|
+
dispatch shape: one action-runner call per selected record (`_rowRecord`
|
|
271
|
+
attached). "Select N rows → ONE call that receives every selected id" — the
|
|
272
|
+
zip-of-QR-codes / merged-PDF / batch-print shape — could not be expressed, so
|
|
273
|
+
downstream projects fell back to per-row `window.open` storms or gave up.
|
|
274
|
+
|
|
275
|
+
`BulkActionDef` now carries `execution?: 'perRecord' | 'aggregate'` (default
|
|
276
|
+
`'perRecord'`, existing views untouched). An aggregate def dispatches its
|
|
277
|
+
action exactly once for the whole selection with `params._selectedIds:
|
|
278
|
+
string[]` injected and the full records published as
|
|
279
|
+
`context.selectedRecords`. The authored form usually just names a declared
|
|
280
|
+
object action — `{ name, operation: 'custom', execution: 'aggregate' }` —
|
|
281
|
+
and `resolveBulkActions` attaches the declaration. Results are
|
|
282
|
+
all-or-nothing: a failure is attributed to every id with the real error and
|
|
283
|
+
per-row Retry is hidden (re-running the action is the retry; a total failure
|
|
284
|
+
keeps the selection). `batchSize` does not apply; `maxRecords` still gates.
|
|
285
|
+
|
|
286
|
+
The executor rides the existing `executeBulkBatch` bulk-first decision tree —
|
|
287
|
+
the aggregate call is its `bulkCall`, and the per-row "fallback" only
|
|
288
|
+
re-throws the captured error for attribution, never fans out N dispatches
|
|
289
|
+
against an endpoint written for one `_selectedIds` call.
|
|
290
|
+
|
|
291
|
+
Also: url/api target interpolation now exposes `${ctx.selection.ids}` (comma
|
|
292
|
+
-joined) and `${ctx.selection.count}` from the grid's checkbox selection, so
|
|
293
|
+
a plain `list_toolbar` action can carry the selection without bulk plumbing;
|
|
294
|
+
the console's server-action handler recognizes `_selectedIds` and skips the
|
|
295
|
+
single-record multi-select guard for aggregate dispatches.
|
|
296
|
+
|
|
297
|
+
- 335041c: Stop declaring 13 `@object-ui/core` symbols under names `@objectstack/spec` owns
|
|
298
|
+
(objectui#3158, objectstack#4115 batch 4).
|
|
299
|
+
|
|
300
|
+
**Breaking for importers of `@object-ui/core`** — seven exported names changed,
|
|
301
|
+
because the spec exports the same name for a _different_ thing:
|
|
302
|
+
|
|
303
|
+
| was | now | what the spec's same-named export actually is |
|
|
304
|
+
| :----------------------- | :-------------------------------- | :--------------------------------------------------------------------------- |
|
|
305
|
+
| `ChartSeries` | `ChartSeriesBinding` | the authored dataset-binding descriptor (a measure `name`, no `data`) |
|
|
306
|
+
| `ActionHandler` | `ActionRunnerHandler` | the SERVER-side objectql handler, `(ctx) => unknown` |
|
|
307
|
+
| `PluginDefinition` | `RegistryPluginDefinition` | the platform PACKAGE manifest (`id`/`slug`/`staticPath`/install hooks) |
|
|
308
|
+
| `ValidationError` | `SchemaNodeValidationError` | plugin-manifest validation, keyed by `field`, no severity |
|
|
309
|
+
| `ValidationResult` | `SchemaNodeValidationResult` | ditto, with both arrays optional |
|
|
310
|
+
| `defineView` | `defineSystemView` | the VIEW-DOCUMENT factory: parses a `ViewSchema`, returns a validated `View` |
|
|
311
|
+
| `resolveCrudAffordances` | `resolveEffectiveCrudAffordances` | the object-level affordance matrix, with no notion of server API operations |
|
|
312
|
+
|
|
313
|
+
The other six keep their names and are now **imported from the spec** instead of
|
|
314
|
+
re-declared: `StyleMap`, `ResponsiveStyles` (ADR-0065), `RowHeight`,
|
|
315
|
+
`CONTEXT_TOKENS`, `CrudAffordances`, `RowCrudPredicates`.
|
|
316
|
+
|
|
317
|
+
**The copies were live misdescriptions, not just duplicates.** Three said so in
|
|
318
|
+
their own comments:
|
|
319
|
+
|
|
320
|
+
- `CONTEXT_TOKENS` carried a note that the duplication was "temporary until the
|
|
321
|
+
next coordinated release… because the installed `@objectstack/spec` predates
|
|
322
|
+
that export". The installed spec (17.0.0-rc.0) exports it, and the copy was
|
|
323
|
+
byte-identical — so it passed every value comparison and every behavioural
|
|
324
|
+
test for the whole interval in which its stated reason was false.
|
|
325
|
+
- `RowHeight` advertised itself as "the spec's `RowHeightSchema` vocabulary"
|
|
326
|
+
while being a hand-written union. It happened to be correct; nothing would
|
|
327
|
+
have caught the day it stopped being.
|
|
328
|
+
- `managedBy.ts` described itself as a "UI-side mirror of the framework's
|
|
329
|
+
`resolveCrudAffordances()`" and carried its own `DEFAULTS` table — a
|
|
330
|
+
line-for-line copy of the spec's `CRUD_AFFORDANCE_DEFAULTS`, plus a copy of
|
|
331
|
+
its override parser.
|
|
332
|
+
|
|
333
|
+
`resolveEffectiveCrudAffordances` now **delegates** the bucket/`userActions` half
|
|
334
|
+
to the spec's `resolveCrudAffordances()`, so the bucket table has exactly one
|
|
335
|
+
definition on the platform. What stays objectui's is the part the spec has no
|
|
336
|
+
notion of: intersecting that matrix with the server-resolved effective API
|
|
337
|
+
operation set (#3391), so the UI never offers a button the server would 405 —
|
|
338
|
+
and the name now says that instead of claiming to be the spec's function.
|
|
339
|
+
|
|
340
|
+
Deriving `RowCrudPredicates` also **tightens** it: the local copy typed
|
|
341
|
+
`visibleWhen`/`disabledWhen` as `unknown`, where the spec types them as
|
|
342
|
+
`Expression | ExpressionInput`. That was imprecision, not a deliberate dialect.
|
|
343
|
+
|
|
344
|
+
- b414983: fix(dashboard): a date globalFilter's preset-name default becomes a range, not an equality
|
|
345
|
+
|
|
346
|
+
Setup → System Overview rendered EVERY KPI tile as 0 while its period selector
|
|
347
|
+
read "All time" (objectstack#4475). Every request was `200 OK`, the widgets
|
|
348
|
+
rendered normally, and nothing in the UI signalled a failure — zeros read as
|
|
349
|
+
"nothing has happened yet" rather than as an error, which is why this survived
|
|
350
|
+
to an RC.
|
|
351
|
+
|
|
352
|
+
Both symptoms are one missing normalization. `resolveDashboardFilterDefs` lifts
|
|
353
|
+
the built-in `dateRange` declaration's preset NAME to `{ preset }`, but passed a
|
|
354
|
+
`globalFilters` entry's `defaultValue` through raw. `@objectstack/spec`'s
|
|
355
|
+
`GlobalFilterSchema.defaultValue` is `string | number | boolean`, so a bare
|
|
356
|
+
preset name is the ONLY spelling an author can write — and nothing ever mapped
|
|
357
|
+
it. System Overview declares
|
|
358
|
+
`{ field: 'created_at', type: 'date', defaultValue: 'last_7_days' }`, so:
|
|
359
|
+
|
|
360
|
+
- `buildFilterCondition` fell through to its "a bare string date means equality
|
|
361
|
+
on that day" branch and the widget sent
|
|
362
|
+
`runtimeFilter: { created_at: 'last_7_days' }`. The backend compiled
|
|
363
|
+
`SELECT COUNT(*) AS "user_count" FROM "sys_user" WHERE created_at = $1`
|
|
364
|
+
— verified against a live server, byte-for-byte the SQL in the issue. The
|
|
365
|
+
actual `sys_user` count is 4; that equality matches no row.
|
|
366
|
+
- `DateRangeFilter` derives its selected item from `value.preset` / `.from` /
|
|
367
|
+
`.to`, all `undefined` on a bare string, so the control fell through to its
|
|
368
|
+
ALL sentinel and displayed "All time" while sending that equality. The tiles
|
|
369
|
+
therefore looked deliberately unfiltered and merely empty.
|
|
370
|
+
|
|
371
|
+
`normalizeDateDefault` now applies the same lift the sibling `dateRange`
|
|
372
|
+
declaration already receives, for `date`/`dateRange` filters whose default names
|
|
373
|
+
a preset this module actually knows. This is not consumer-side leniency: it is
|
|
374
|
+
one normalization function completing the same conversion for the sibling
|
|
375
|
+
declaration, and the spec admits no other spelling for an author to fix at the
|
|
376
|
+
producer. A genuine ISO date string still means equality on that day (the
|
|
377
|
+
documented behaviour), and numbers, booleans and unrecognised strings are left
|
|
378
|
+
exactly as declared.
|
|
379
|
+
|
|
380
|
+
No backend change is needed: given a real range the dataset path already lowers
|
|
381
|
+
it correctly (`WHERE (created_at >= $1 AND created_at < $2)` → 4). The
|
|
382
|
+
framework's dashboard metadata needs none either — it is spec-compliant as
|
|
383
|
+
written, and editing it would only hide the defect.
|
|
384
|
+
|
|
385
|
+
Levelled `minor` rather than `patch` because the change is visible in rendered
|
|
386
|
+
dashboards rather than internal: any dashboard declaring a date-typed
|
|
387
|
+
`globalFilters` default now emits a different query shape, its numbers change
|
|
388
|
+
(from 0 to real values), and its filter control's displayed label changes with
|
|
389
|
+
them. Anything asserting on the previously-emitted condition will see it move.
|
|
390
|
+
|
|
391
|
+
Known residual, filed separately rather than widened into here: a `date` filter
|
|
392
|
+
whose value is neither a known preset nor a parseable ISO date still degrades
|
|
393
|
+
silently to an equality that matches nothing, producing the same
|
|
394
|
+
healthy-looking zero. Preset names are covered by this change; a misspelled
|
|
395
|
+
custom value is not.
|
|
396
|
+
|
|
397
|
+
- 256f8cc: fix(dashboard): an unrecognised date filter value is skipped and named, not compared
|
|
398
|
+
|
|
399
|
+
The residual the preset-name fix (objectui#3150 / objectstack#4475) left behind,
|
|
400
|
+
and the more deceptive half of it: a `date`/`dateRange` filter value that is
|
|
401
|
+
neither a known preset name nor a parseable date used to fall through to the
|
|
402
|
+
"a bare string date means equality on that day" branch. A misspelled default —
|
|
403
|
+
`defaultValue: 'last_7_dayz'` — therefore reached the widget query as
|
|
404
|
+
`runtimeFilter: { created_at: 'last_7_dayz' }`, which the backend faithfully
|
|
405
|
+
compiled to `WHERE created_at = $1`. `200 OK`, widget renders, count is 0 —
|
|
406
|
+
indistinguishable from "this range genuinely has no data". No 4xx, no console
|
|
407
|
+
warning, no UI signal. objectstack#4475 took a full RC cycle to catch for
|
|
408
|
+
exactly this reason: **0 looks like a legitimate answer**.
|
|
409
|
+
|
|
410
|
+
`buildFilterCondition` now holds a date value to three spellings, and only
|
|
411
|
+
three:
|
|
412
|
+
|
|
413
|
+
1. a known preset name → range bounds (unchanged, objectui#3150);
|
|
414
|
+
2. an ISO date (`2026-01-15`, `2026-01-15T08:30:00Z`) or a date-macro token
|
|
415
|
+
(`{today}`, `{7_days_ago}`) → equality on that day (the documented
|
|
416
|
+
behaviour, unchanged);
|
|
417
|
+
3. **anything else → the filter is skipped and `console.warn` names the filter,
|
|
418
|
+
the offending value, and the accepted spellings.**
|
|
419
|
+
|
|
420
|
+
The `{ preset: '<unknown>' }` object form gets the same voice. It already
|
|
421
|
+
dropped the filter — silently — because the preset lookup missed and no
|
|
422
|
+
`from`/`to` remained; that drop is now announced. When explicit bounds ride
|
|
423
|
+
along with an unknown preset the bounds are still honoured, and the warning says
|
|
424
|
+
which of the two won.
|
|
425
|
+
|
|
426
|
+
Rule 3 is deliberately the same strictness `buildWidgetScopedFilter` already
|
|
427
|
+
applies to a _default binding on a field the object does not have_ — skip and
|
|
428
|
+
warn, with the same rationale spelled out there: never emit a query the backend
|
|
429
|
+
can only empty-match. Field _names_ had that guard; field _values_ did not.
|
|
430
|
+
|
|
431
|
+
The macro-token check asks `resolveDateMacros` itself whether it recognises the
|
|
432
|
+
string, rather than restating its token grammar in a second place. One
|
|
433
|
+
vocabulary, no dialect to drift — and a token that resolver does not know
|
|
434
|
+
(`{last_7_dayz}`) is precisely the typo this guard exists to catch.
|
|
435
|
+
|
|
436
|
+
Levelled `minor`, matching objectui#3150, because the emitted query shape
|
|
437
|
+
changes: a dashboard carrying a misspelled date value stops sending a
|
|
438
|
+
never-matching equality and instead sends no constraint for that filter (its
|
|
439
|
+
numbers go from 0 to unfiltered) while the console says why. Anything asserting
|
|
440
|
+
on the previously-emitted equality will see it disappear.
|
|
441
|
+
|
|
442
|
+
Note the direction of the relaxation is chosen, not incidental: skipping widens
|
|
443
|
+
the result set, so the number visibly changes and the warning explains it —
|
|
444
|
+
whereas the old behaviour narrowed it to zero, which is the one outcome an
|
|
445
|
+
author cannot tell from a correct answer. Author-time rejection (validating
|
|
446
|
+
`GlobalFilterSchema.defaultValue` at publish, in `@objectstack/spec`) is the
|
|
447
|
+
stricter complement and belongs on the platform side; it is filed separately.
|
|
448
|
+
|
|
449
|
+
- d3584c6: Bring the whole `@objectstack` family to `17.0.0-rc.1`, so the dependency graph resolves a
|
|
450
|
+
single copy of `@objectstack/spec`.
|
|
451
|
+
|
|
452
|
+
#3178 bumped **only** `@objectstack/spec` to `17.0.0-rc.1`. The rest of the family —
|
|
453
|
+
`client`, `core`, `formula`, `lint` (and `sdui-parser`, reached through `lint`) — stayed on
|
|
454
|
+
`17.0.0-rc.0`, and each of them depends on spec at an **exact** version rather than a
|
|
455
|
+
caret:
|
|
456
|
+
|
|
457
|
+
```
|
|
458
|
+
@objectstack/client@17.0.0-rc.0 -> spec "17.0.0-rc.0"
|
|
459
|
+
@objectstack/core@17.0.0-rc.0 -> spec "17.0.0-rc.0"
|
|
460
|
+
@objectstack/formula@17.0.0-rc.0 -> spec "17.0.0-rc.0"
|
|
461
|
+
@objectstack/lint@17.0.0-rc.0 -> spec "17.0.0-rc.0"
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
So `main` carried **two** spec copies: objectui's own code read `17.0.0-rc.1` while every
|
|
465
|
+
`@objectstack/*` package read `17.0.0-rc.0` from its own nested `node_modules`. That breaks
|
|
466
|
+
the single-contract invariant this repo's guards are built on, and it breaks them
|
|
467
|
+
_silently_ — the affected checks depend on identity, not on version strings:
|
|
468
|
+
|
|
469
|
+
- `spec-subschema-parity.test.ts` distinguishes a genuine re-export from a fork by
|
|
470
|
+
**reference identity** of the zod schema object. Two spec copies make every schema a
|
|
471
|
+
distinct object, so a real re-export starts reading as a fork (or a fork slips through,
|
|
472
|
+
depending on which copy each side resolved).
|
|
473
|
+
- `scripts/check-spec-symbol-derivation.mjs` and `spec-symbol-parity.test.ts` use
|
|
474
|
+
`createRequire` to resolve spec's `.d.ts` and run it through the TS checker. With two
|
|
475
|
+
copies installed, _which_ declaration file the checker sees is a function of resolution
|
|
476
|
+
order rather than of intent.
|
|
477
|
+
|
|
478
|
+
The declared ranges were already `^17.0.0-rc.0`, which technically admits rc.1 — the pin
|
|
479
|
+
lived in the lockfile. Raising the remaining ranges to `^17.0.0-rc.1` makes the floor
|
|
480
|
+
explicit and forbids a future install from silently sliding back onto a family member that
|
|
481
|
+
drags rc.0 along with it. The rc.1 family members pin spec at `17.0.0-rc.1` exactly, so the
|
|
482
|
+
graph now converges on one copy by construction, not by luck.
|
|
483
|
+
|
|
484
|
+
No product behaviour changes here. `check:spec-symbols` reconciliation was already
|
|
485
|
+
completed by #3178 and stays green under the unified graph; this changeset is `minor`
|
|
486
|
+
per the repo's fixed-group version policy.
|
|
487
|
+
|
|
488
|
+
- 444457c: feat!: follow the framework's `managedBy: 'system'` → `'system-data'` retirement (objectstack#3355)
|
|
489
|
+
|
|
490
|
+
**FROM → TO: `managedBy: 'system'` → `managedBy: 'system-data'`.** The framework
|
|
491
|
+
retired the residual `system` bucket in protocol 17; this is the UI half of that
|
|
492
|
+
change, landing with it so the closed `ManagedByBucket` union stays a mirror
|
|
493
|
+
rather than a fork.
|
|
494
|
+
|
|
495
|
+
ADR-0103 split the overloaded `system` bucket additively in v16 — the
|
|
496
|
+
engine-owned objects moved to the explicit `engine-owned`, the admin/user-writable
|
|
497
|
+
ones stayed on `system` — which left that value named after the half that had
|
|
498
|
+
already moved out. `system-data` names what it actually holds: the SCHEMA is the
|
|
499
|
+
platform's, the DATA is the admin's or the user's.
|
|
500
|
+
|
|
501
|
+
**The derivation this deletes is the point.** Because v16's `system` doubled as
|
|
502
|
+
both the engine-owned default and the writable set, three UI surfaces had to
|
|
503
|
+
RECOVER the distinction from `userActions` at render time:
|
|
504
|
+
|
|
505
|
+
- `isSystemWritable()` probed `userActions` for any opted-in write. It is now
|
|
506
|
+
`managedBy === 'system-data'` — the bucket answers directly.
|
|
507
|
+
- `ManagedByBadge` derived a synthetic `'system-writable'` variant key. The
|
|
508
|
+
variant map is now 1:1 with the bucket union, so a new bucket is a compile
|
|
509
|
+
error to miss instead of a silent fallthrough. The `systemWritable` /
|
|
510
|
+
`system` i18n keys are **unchanged**, so no locale bundle moves.
|
|
511
|
+
- `resolveManagedByEmptyState()` asked the resolved `create` affordance whether a
|
|
512
|
+
`system` list should read "entries appear automatically" or show the New
|
|
513
|
+
button. `system-data` now falls through to the generic empty state by
|
|
514
|
+
definition; `engine-owned` keeps the automatic-entries copy.
|
|
515
|
+
|
|
516
|
+
**Breaking (UI API):** `ManagedByBadge`'s `userActions` prop and the exported
|
|
517
|
+
`ManagedByUserActions` interface are **removed**. The bucket alone selects the
|
|
518
|
+
variant now, so the prop had become metadata nothing read — the exact defect the
|
|
519
|
+
framework change exists to remove; shipping it as an accepted-but-ignored prop
|
|
520
|
+
would have reproduced it one layer up. Drop the prop from call sites; no other
|
|
521
|
+
change is needed.
|
|
522
|
+
|
|
523
|
+
`MANAGED_BY_BUCKETS` and `ManagedByBucket` no longer contain `'system'`.
|
|
524
|
+
|
|
525
|
+
- 850033c: Stop offering the retired `action.shortcut` / `action.bulkEnabled` keys.
|
|
526
|
+
|
|
527
|
+
`@objectstack/spec` 17 retired both as `retiredKey()` tombstones: authoring
|
|
528
|
+
either one is a hard PARSE REJECTION, so a draft carrying it cannot be saved
|
|
529
|
+
at all. The designer still offered controls for both — a "Bulk — apply to
|
|
530
|
+
multiple selected rows" checkbox and a "Shortcut" text field — which meant the
|
|
531
|
+
Studio action inspector let an author build a draft the platform would then
|
|
532
|
+
refuse, with the rejection arriving later and nowhere near the checkbox.
|
|
533
|
+
|
|
534
|
+
- **Action inspector**: both controls removed. The keys stay hidden from the
|
|
535
|
+
fallback form (the server's live schema still advertises them, so dropping
|
|
536
|
+
them from the hidden list would put the inputs straight back) — now under a
|
|
537
|
+
`RETIRED_FIELDS` list that says why, so nobody "restores the missing
|
|
538
|
+
control". `bulkEnabled`'s replacement is the list view's `bulkActions` /
|
|
539
|
+
`bulkActionDefs`; `shortcut` has none.
|
|
540
|
+
- **Action preview**: the `shortcut` and `bulk` pills are gone — they could
|
|
541
|
+
only ever render for metadata the platform now refuses.
|
|
542
|
+
- **`ActionEngine.registerActions`**: no longer harvests the two retired keys
|
|
543
|
+
from authored metadata, which made two dead registration options look
|
|
544
|
+
load-bearing. Both are still accepted on the single-action
|
|
545
|
+
`registerAction(action, options)` overload, where a HOST passes them
|
|
546
|
+
explicitly.
|
|
547
|
+
|
|
548
|
+
- 009e25d: Report / chart / query symbols stop wearing `@objectstack/spec`'s names
|
|
549
|
+
(objectui#3155, objectstack#4115).
|
|
550
|
+
|
|
551
|
+
**Breaking for TypeScript imports** — six exported names change. Each was a
|
|
552
|
+
different concept than the spec export it collided with, so an author reading
|
|
553
|
+
the objectui declaration as "the spec's" was reading a false claim:
|
|
554
|
+
|
|
555
|
+
| was | now | why they were never the same thing |
|
|
556
|
+
| :------------------ | :----------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
557
|
+
| `ChartSeries` | `ChartDataSeries` | ours is a display name plus literal `data: number[]`; the spec's is a dataset-bound series descriptor (`type`/`stack`/`yAxis`/`variant`) with no data at all |
|
|
558
|
+
| `ChartSeriesSchema` | `ChartDataSeriesSchema` | zod twin of the above |
|
|
559
|
+
| `QueryAST` | `SqlQueryAST` | ours is a compiled SQL syntax tree (`select`/`from`/`join`/`group_by`); the spec's is the ObjectQL request descriptor (`object`/`fields`/`where`/`expand`) |
|
|
560
|
+
| `QuerySchema` | `DriverQueryConfig` | ours is the high-level config `QueryASTBuilder` compiles; the spec exports that name as a zod schema value |
|
|
561
|
+
| `DriverInterface` | `SqlDriverInterface` | ours is objectui's SQL-oriented client abstraction (`query(sql, params)`); the spec's is the platform runtime driver contract |
|
|
562
|
+
| `DatasourceSchema` | `DatasourceRegistration` | ours is the in-memory record `DatasourceManager` holds — its `driver` is a live instance; the spec's is the authored metadata document, where `driver` is a name |
|
|
563
|
+
|
|
564
|
+
Three more are now DERIVED from the spec instead of hand-restated, which fixes
|
|
565
|
+
live silent-stripping defects, since a `z.object()` drops unknown keys:
|
|
566
|
+
|
|
567
|
+
- **`DashboardWidgetSchema`** declared 10 of the spec's 22 keys, so
|
|
568
|
+
`objectui validate` deleted the other 12 without a word — `chartConfig`,
|
|
569
|
+
`colorVariant`, `filter`, `responsive`, `aria`,
|
|
570
|
+
`actionUrl`/`actionType`/`actionIcon`, `compareTo`, `suppressWarnings` and the
|
|
571
|
+
`requiresObject` / `requiresService` capability gates the dashboard renderer
|
|
572
|
+
honours at runtime. The TS interface had declared most of them all along, so a
|
|
573
|
+
widget could type-check and still lose half its configuration on validation.
|
|
574
|
+
Pinned divergences kept: `id` stays optional, `type` stays widened for the
|
|
575
|
+
objectui-only `list` / `custom` families, and the legacy `component` envelope
|
|
576
|
+
stays.
|
|
577
|
+
- **`GlobalFilterSchema`** took `scope` as a free-form string (any typo
|
|
578
|
+
validated); it now uses the spec's `widget | dashboard` vocabulary. The three
|
|
579
|
+
objectui widenings that back a real runtime normalizer are kept and pinned:
|
|
580
|
+
the bare-string `options` shorthand, the normalized `{ preset }` date default,
|
|
581
|
+
and an optional `optionsFrom.labelField`.
|
|
582
|
+
- **`AppContextSelectorSchema`** was a full restatement; spec keys and their
|
|
583
|
+
defaults now flow in by reference, with `label` widened for objectui's i18n
|
|
584
|
+
label envelope — which `AppContextSelectors` already renders.
|
|
585
|
+
|
|
586
|
+
`ListViewSchema`'s zod node now names the spec in its own initializer rather
|
|
587
|
+
than one hop away through a local const, so its long-standing derivation is
|
|
588
|
+
visible where it is declared.
|
|
589
|
+
|
|
590
|
+
Drift guard: `packages/types/src/__tests__/report-chart-query-spec-parity.test.ts`.
|
|
591
|
+
|
|
592
|
+
- 726b89c: `@object-ui/types` stops declaring sixteen symbols under names `@objectstack/spec` owns (objectui#3156, objectstack#4115).
|
|
593
|
+
|
|
594
|
+
Seven are now **derived** from the spec, nine are **renamed** to the local
|
|
595
|
+
dialect they always were. Both halves remove the same hazard: a local
|
|
596
|
+
declaration under a spec export's name reads as the spec's own definition to
|
|
597
|
+
the next reader, so a copy that is merely _correct today_ is a planted premise
|
|
598
|
+
tomorrow.
|
|
599
|
+
|
|
600
|
+
**Derived** — the spec now supplies the keys, by reference:
|
|
601
|
+
|
|
602
|
+
| symbol | derivation |
|
|
603
|
+
| :----------------------- | :-------------------------------------------------------------------------------- |
|
|
604
|
+
| `ActionParam` | `z.input<typeof ActionParamSchema>`, `type` widened to the local legacy spellings |
|
|
605
|
+
| `CreateExportJobRequest` | `Omit<CreateExportJobInput, 'object'>` (`object` is the method argument) |
|
|
606
|
+
| `CreateExportJobResult` | re-export from `@objectstack/spec/contracts` |
|
|
607
|
+
| `ImportRowResult` | re-export from `@objectstack/spec/api` |
|
|
608
|
+
| `NavigationArea` | spec keys, with `navigation` / `visible` pinned locally |
|
|
609
|
+
| `NavigationAreaSchema` | `specFieldsExcept(NavigationAreaSchema.shape, …)` |
|
|
610
|
+
| `Theme` | re-export of the spec's `ThemeInput` (the authoring shape) |
|
|
611
|
+
| `ExportJobFormat` | re-export of the spec's `ExportFormat` |
|
|
612
|
+
|
|
613
|
+
Four of these close real gaps rather than tidy names. `ActionParam` never
|
|
614
|
+
declared `reference` — the key `resolveActionParams()` actually reads for an
|
|
615
|
+
inline lookup target — nor `defaultFromRow`, which the metadata designer's own
|
|
616
|
+
inspector writes; it also narrowed `visible` to a bare string although the
|
|
617
|
+
resolver has always accepted the `{ dialect, source }` envelope too.
|
|
618
|
+
`CreateExportJobResult.createdAt` and `ImportRowResult.action` were optional
|
|
619
|
+
here and required by the server, leaving every consumer a branch that could
|
|
620
|
+
never run. And `NavigationArea`'s `id` now carries the spec's own length rule
|
|
621
|
+
instead of accepting any string.
|
|
622
|
+
|
|
623
|
+
**Renamed** — same word, different concept:
|
|
624
|
+
|
|
625
|
+
| was | now | why |
|
|
626
|
+
| :----------------- | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
|
|
627
|
+
| `FileMetadata` | `UploadedFileMetadata` | field-VALUE payload (`url`, `original_name`), not the storage file record |
|
|
628
|
+
| `GestureType` | `TouchGestureType` | direction-fused (`swipe-left`), not the spec's type+direction pair |
|
|
629
|
+
| `GestureConfig` | `TouchGestureConfig` | gesture→`action` binding, not per-gesture tuning |
|
|
630
|
+
| `OfflineConfig` | `PWAOfflineConfig` | service-worker route caching, not the offline data/sync model |
|
|
631
|
+
| `PageRegion` | `PageNodeRegion` | region of the renderer page NODE, holding `SchemaNode`s |
|
|
632
|
+
| `PageRegionSchema` | `PageNodeRegionSchema` | zod twin of the above |
|
|
633
|
+
| `ResponsiveConfig` | `MobileResponsiveConfig` | mobile box config, not the spec's SDUI grid contract |
|
|
634
|
+
| `WidgetManifest` | `RuntimeWidgetManifest` | SDUI component manifest, not the field-widget plugin manifest |
|
|
635
|
+
| `WidgetSource` | `RuntimeWidgetSource` | `module`/`inline`/`registry` loader union — and its `inline` carries a resolved component where the spec's carries source code |
|
|
636
|
+
|
|
637
|
+
**Migration**: the old names are gone, not deprecated — an alias would preserve
|
|
638
|
+
exactly the ambiguity being removed. Import the new name; nothing about the
|
|
639
|
+
shapes changed. `@object-ui/types` already re-exports the spec's own
|
|
640
|
+
`SpecResponsiveConfig`, and `@object-ui/react`'s `useOffline` config remains the
|
|
641
|
+
spec-shaped `OfflineConfig`, so both concepts stay reachable under
|
|
642
|
+
distinguishable names.
|
|
643
|
+
|
|
644
|
+
Each rename carries a bidirectional tripwire
|
|
645
|
+
(`packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts`): it fails if
|
|
646
|
+
the spec ever claims the new name, and also if the spec retires the old one —
|
|
647
|
+
at which point the natural name can be taken back rather than the workaround
|
|
648
|
+
outliving its reason.
|
|
649
|
+
|
|
650
|
+
### Patch Changes
|
|
651
|
+
|
|
652
|
+
- d9668a7: Honor the server's declared percent scale, so a ratio of exactly 1 renders as 100.0% (#3136)
|
|
653
|
+
|
|
654
|
+
A dataset measure declared `format: '0.0%'` rendered every ratio below 1
|
|
655
|
+
correctly and got the single most consequential one wrong: a rate of exactly
|
|
656
|
+
`1` printed as **`1.0%`**. On an SLA / pass-rate dashboard that turns
|
|
657
|
+
"everything met the SLA" into "1% met the SLA", on both surfaces the issue
|
|
658
|
+
names — the KPI card and the dataset-bound table (they share `formatMeasure`).
|
|
659
|
+
|
|
660
|
+
The cause was never a bad multiplier; it was a missing fact. `formatMeasure`
|
|
661
|
+
scaled by magnitude — `percentDisplayValue` multiplies by 100 only strictly
|
|
662
|
+
inside `(-1, 1)` — because the column arrived with a `%` format string and
|
|
663
|
+
nothing saying what scale its numbers were on. That guess is undecidable at
|
|
664
|
+
exactly 1, which is both a full-compliance ratio ("100%") and one percentage
|
|
665
|
+
point ("1%"), and it resolved to the reading almost nobody means.
|
|
666
|
+
|
|
667
|
+
The server now answers the question instead (framework: `percentScaleOf` +
|
|
668
|
+
`AnalyticsResult.fields[].percentScale`, the sibling of the ADR-0053 currency
|
|
669
|
+
chain): a `derived: { op: 'ratio' }` measure is a `fraction` by definition, and
|
|
670
|
+
a measure over a `percent` field inherits that field's scale. `formatMeasure`
|
|
671
|
+
takes the declared scale as a fourth argument and, when present, scales by it —
|
|
672
|
+
`fraction` ×100, `whole` verbatim — instead of inspecting the value. Every
|
|
673
|
+
dataset-bound call site passes the column's `percentScale`: the dashboard
|
|
674
|
+
metric/table/pivot cells, the report renderer's cells, totals and KPI, and the
|
|
675
|
+
dataset preview.
|
|
676
|
+
|
|
677
|
+
`percentDisplayValue` is untouched and still the fallback for a column that
|
|
678
|
+
arrives without the annotation (an older server, or a non-dataset percent cell
|
|
679
|
+
in a list view), so nothing that renders correctly today changes.
|
|
680
|
+
|
|
681
|
+
- a8ad6c0: A required boolean must be savable in its UNCHECKED state — `false` and `0` are values.
|
|
682
|
+
|
|
683
|
+
Reported against an AI-built task tracker whose 任务 object has a required
|
|
684
|
+
`是否完成` boolean: the create form showed the switch OFF, answered "是否完成不能
|
|
685
|
+
为空", and saved instantly once the switch was turned ON. The app could only ever
|
|
686
|
+
create ALREADY-DONE tasks — the one state the control shows by default was the
|
|
687
|
+
one value it refused to save (cloud#972).
|
|
688
|
+
|
|
689
|
+
Two defects stacked, and either alone is enough to break it:
|
|
690
|
+
|
|
691
|
+
**The `required` verdict read truthiness, not presence.** `@objectstack/spec`
|
|
692
|
+
FieldSchema.required (ADR-0113) is "an insert must provide a NON-NULL value",
|
|
693
|
+
and objectql's record validator implements exactly that. react-hook-form's
|
|
694
|
+
built-in rule instead fails whenever `isBoolean(value) && !value` — its
|
|
695
|
+
accept-the-terms checkbox heritage — silently redefining every required boolean
|
|
696
|
+
as "must be TRUE", including a select whose chosen option value is `false`. It
|
|
697
|
+
also disagreed the other way, letting a whitespace-only string through for the
|
|
698
|
+
server to reject with a 400. The form renderer no longer hands RHF its own
|
|
699
|
+
`required`: the check is now a `validate` entry keyed `required` (so the error
|
|
700
|
+
still surfaces as `type: 'required'`, which the conditional-required cleanup
|
|
701
|
+
keys on) backed by a new shared `isMissingForRequired` in `@object-ui/core`, a
|
|
702
|
+
deliberate mirror of objectql `record-validator.isMissing` — `undefined`,
|
|
703
|
+
`null`, blank-after-trim string, empty array. Deleting the inherited rule also
|
|
704
|
+
stops a `required` that rode in on `validation` from outliving a `requiredWhen`
|
|
705
|
+
that resolved to FALSE.
|
|
706
|
+
|
|
707
|
+
**A boolean field held `undefined` while displaying "off".** A two-state control
|
|
708
|
+
has no third state, but a field with no entry in `defaultValues` rendered an OFF
|
|
709
|
+
switch backed by nothing: the create payload omitted the column (it lands null,
|
|
710
|
+
which reads as unchecked but isn't) and the presence check above would still
|
|
711
|
+
refuse it. The form renderer now folds `false` into `defaultValues` for every
|
|
712
|
+
boolean-widget field the caller left unset — in `defaultValues` itself, not
|
|
713
|
+
per-Controller, because that object is also the dirty-check baseline and what
|
|
714
|
+
the defaults-reset window replays. Every surface gets it, including the
|
|
715
|
+
modal/drawer create dialogs that start from a bare `{}`. An authored default
|
|
716
|
+
(or a loaded record, `null` included) still wins.
|
|
717
|
+
|
|
718
|
+
`WizardForm`'s cross-step gate had its own copy of the empty-value predicate; it
|
|
719
|
+
now imports the shared one so it cannot drift from the per-field verdict. And
|
|
720
|
+
the field-demo renderer read `schema.defaultValue || schema.value`, throwing
|
|
721
|
+
away an authored default of `false` / `0` / `''` — same falsy-as-empty class,
|
|
722
|
+
now `??`.
|
|
723
|
+
|
|
724
|
+
Verified end to end on a local stack against the exact metadata shape
|
|
725
|
+
`apply_blueprint` materializes (`{ type: 'boolean', required: true }`, no
|
|
726
|
+
default): a 是否完成 = 否 task with 工时 = 0 now creates and persists as
|
|
727
|
+
`{ hours: 0, is_done: false }`, turning the switch on still stores `true`, and a
|
|
728
|
+
blank required text is still refused.
|
|
729
|
+
|
|
730
|
+
- Updated dependencies [4ae0ac4]
|
|
731
|
+
- Updated dependencies [696e3c1]
|
|
732
|
+
- Updated dependencies [4bf612c]
|
|
733
|
+
- Updated dependencies [cb82705]
|
|
734
|
+
- Updated dependencies [f572849]
|
|
735
|
+
- Updated dependencies [444457c]
|
|
736
|
+
- Updated dependencies [022e4c3]
|
|
737
|
+
- Updated dependencies [009e25d]
|
|
738
|
+
- Updated dependencies [726b89c]
|
|
739
|
+
- @object-ui/types@17.2.0
|
|
740
|
+
|
|
3
741
|
## 17.1.0
|
|
4
742
|
|
|
5
743
|
### Minor Changes
|