@object-ui/core 16.1.0 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1881 -4
- package/dist/actions/ActionEngine.d.ts +11 -1
- package/dist/actions/ActionEngine.js +14 -3
- package/dist/actions/ActionRunner.d.ts +199 -24
- package/dist/actions/ActionRunner.js +189 -84
- package/dist/actions/actionKeys.d.ts +135 -0
- package/dist/actions/actionKeys.js +283 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/adapters/ApiDataSource.js +10 -1
- package/dist/adapters/ValueDataSource.d.ts +2 -2
- package/dist/evaluator/fieldRules.d.ts +0 -2
- package/dist/evaluator/fieldRules.js +3 -2
- package/dist/evaluator/optionLint.js +5 -2
- package/dist/evaluator/optionRules.d.ts +13 -6
- package/dist/evaluator/optionRules.js +0 -23
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- 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/Registry.d.ts +81 -2
- package/dist/registry/Registry.js +102 -2
- package/dist/registry/WidgetRegistry.d.ts +7 -7
- package/dist/registry/WidgetRegistry.js +1 -1
- package/dist/registry/public-blocks.js +52 -1
- package/dist/styling/scoped-styles.d.ts +14 -8
- package/dist/theme/ThemeEngine.js +0 -12
- package/dist/utils/chart-series.d.ts +42 -2
- package/dist/utils/chart-series.js +55 -0
- package/dist/utils/column-identity.d.ts +110 -0
- package/dist/utils/column-identity.js +210 -0
- package/dist/utils/dashboard-filters.d.ts +11 -2
- package/dist/utils/dashboard-filters.js +99 -4
- package/dist/utils/dataset-format.d.ts +13 -1
- package/dist/utils/dataset-format.js +10 -2
- package/dist/utils/expand-fields.js +8 -8
- package/dist/utils/filter-converter.d.ts +47 -0
- package/dist/utils/filter-converter.js +94 -7
- package/dist/utils/filter-tokens.d.ts +108 -0
- package/dist/utils/filter-tokens.js +180 -0
- package/dist/utils/freeze-schema.d.ts +14 -6
- package/dist/utils/freeze-schema.js +14 -6
- package/dist/utils/managedBy.d.ts +97 -51
- package/dist/utils/managedBy.js +117 -53
- package/dist/utils/normalize-list-view.d.ts +109 -0
- package/dist/utils/normalize-list-view.js +255 -0
- package/dist/utils/sort-values.d.ts +52 -0
- package/dist/utils/sort-values.js +125 -0
- package/dist/validation/index.d.ts +10 -1
- package/dist/validation/index.js +10 -1
- 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/dist/validation/validators/index.d.ts +7 -1
- package/dist/validation/validators/index.js +7 -1
- package/dist/validation/validators/object-validation-engine.d.ts +151 -31
- package/dist/validation/validators/object-validation-engine.js +275 -142
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1882 @@
|
|
|
1
1
|
# @object-ui/core
|
|
2
2
|
|
|
3
|
+
## 17.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bca45cc: Declare the 18 spec-owned action keys `ActionDef` had been absorbing silently.
|
|
8
|
+
|
|
9
|
+
`ActionDef` ends with `[key: string]: any`, so it accepted any key of any type —
|
|
10
|
+
a typo (`targt`) and a retired spec key (`execute`) both type-checked, then the
|
|
11
|
+
runner silently bound no handler (the #2169 "Mark Done does nothing" shape).
|
|
12
|
+
Step 1 (objectstack#4075) made that audible with a dev-mode warning. This is
|
|
13
|
+
step 2: the keys the warning identified as legitimate are now real fields.
|
|
14
|
+
|
|
15
|
+
- **18 keys promoted to explicit optional fields** — `ai`, `aria`, `bodyExtra`,
|
|
16
|
+
`bodyShape`, `bulkEnabled`, `component`, `icon`, `locations`, `mode`,
|
|
17
|
+
`objectName`, `order`, `recordIdField`, `recordIdParam`, `requiredPermissions`,
|
|
18
|
+
`requiresFeature`, `shortcut`, `variant`, `visible`. Every type is **derived**
|
|
19
|
+
from `@objectstack/spec`'s `ActionInput` (`SpecActionInput['locations']`, …),
|
|
20
|
+
never hand-copied: a hand-written duplicate of a spec shape is a second
|
|
21
|
+
contract that drifts, which is the failure this issue is about. Wrong-typed
|
|
22
|
+
values are now compile errors — `order: 'first'`, `variant: 'chartreuse'`,
|
|
23
|
+
`locations: ['nope']` — where before they were absorbed silently.
|
|
24
|
+
- **Derived from `z.input`, not `z.infer`.** `ActionSchema` is a `ZodPipe` whose
|
|
25
|
+
transform narrows `visible` from `string | { dialect, source }` to the
|
|
26
|
+
envelope alone. This runner consumes authored/stored rows, which are
|
|
27
|
+
rehydrated unparsed, so it sees the input shape; deriving from the inferred
|
|
28
|
+
`Action` would have rejected the raw-string predicate `ActionEngine`
|
|
29
|
+
explicitly supports.
|
|
30
|
+
- **Three `as any` casts deleted** in `ActionEngine` — `visible` and
|
|
31
|
+
`requiredPermissions` at the location filter, `locations` at registration.
|
|
32
|
+
They existed only because the fields were undeclared.
|
|
33
|
+
- **Four objectui-dialect keys marked `@deprecated`** with the spec spelling to
|
|
34
|
+
use instead — `actionType` (→ `type`), `api` and `endpoint` (→ `target`;
|
|
35
|
+
`executeAPI` already resolves `api || endpoint || target`), and `navigate`
|
|
36
|
+
(→ flat `target` / `openIn`). Only these four: the remaining dialect keys are
|
|
37
|
+
runner mechanics (chaining, toasts, post-execution reload/close) with no spec
|
|
38
|
+
counterpart, and pointing them at a spelling that does not exist would be
|
|
39
|
+
worse than leaving them declared.
|
|
40
|
+
|
|
41
|
+
**Breaking edge, deliberate.** `shortcut` and `bulkEnabled` were retired by
|
|
42
|
+
`@objectstack/spec` 17 as `retiredKey()` tombstones (`z.never()`), so authoring
|
|
43
|
+
either is already a hard parse rejection. Deriving their types rather than
|
|
44
|
+
hand-writing them turns that runtime rejection into a **compile error**: code
|
|
45
|
+
that assigned `shortcut: 'ctrl+k'` to an `ActionDef` compiled before and does
|
|
46
|
+
not now. Such metadata was already refused by the platform — this only moves the
|
|
47
|
+
failure to where it can be fixed. A host may still pass either explicitly via
|
|
48
|
+
`ActionEngine.registerAction(action, { shortcut, bulkEnabled })`; only authored
|
|
49
|
+
metadata stopped carrying them. `bulkEnabled`'s replacement is the list view's
|
|
50
|
+
`bulkActions` / `bulkActionDefs`; `shortcut` has none.
|
|
51
|
+
|
|
52
|
+
The index signature **stays** — removing it is step 3, and the inverted pin
|
|
53
|
+
asserting it is still present remains the issue's own completion check.
|
|
54
|
+
|
|
55
|
+
- 4bf612c: Aggregate single-call mode for bulk actions: `execution: 'aggregate'` (objectui#3139).
|
|
56
|
+
|
|
57
|
+
A `bulkActionDefs` entry with `operation: 'custom'` used to have exactly one
|
|
58
|
+
dispatch shape: one action-runner call per selected record (`_rowRecord`
|
|
59
|
+
attached). "Select N rows → ONE call that receives every selected id" — the
|
|
60
|
+
zip-of-QR-codes / merged-PDF / batch-print shape — could not be expressed, so
|
|
61
|
+
downstream projects fell back to per-row `window.open` storms or gave up.
|
|
62
|
+
|
|
63
|
+
`BulkActionDef` now carries `execution?: 'perRecord' | 'aggregate'` (default
|
|
64
|
+
`'perRecord'`, existing views untouched). An aggregate def dispatches its
|
|
65
|
+
action exactly once for the whole selection with `params._selectedIds:
|
|
66
|
+
string[]` injected and the full records published as
|
|
67
|
+
`context.selectedRecords`. The authored form usually just names a declared
|
|
68
|
+
object action — `{ name, operation: 'custom', execution: 'aggregate' }` —
|
|
69
|
+
and `resolveBulkActions` attaches the declaration. Results are
|
|
70
|
+
all-or-nothing: a failure is attributed to every id with the real error and
|
|
71
|
+
per-row Retry is hidden (re-running the action is the retry; a total failure
|
|
72
|
+
keeps the selection). `batchSize` does not apply; `maxRecords` still gates.
|
|
73
|
+
|
|
74
|
+
The executor rides the existing `executeBulkBatch` bulk-first decision tree —
|
|
75
|
+
the aggregate call is its `bulkCall`, and the per-row "fallback" only
|
|
76
|
+
re-throws the captured error for attribution, never fans out N dispatches
|
|
77
|
+
against an endpoint written for one `_selectedIds` call.
|
|
78
|
+
|
|
79
|
+
Also: url/api target interpolation now exposes `${ctx.selection.ids}` (comma
|
|
80
|
+
-joined) and `${ctx.selection.count}` from the grid's checkbox selection, so
|
|
81
|
+
a plain `list_toolbar` action can carry the selection without bulk plumbing;
|
|
82
|
+
the console's server-action handler recognizes `_selectedIds` and skips the
|
|
83
|
+
single-record multi-select guard for aggregate dispatches.
|
|
84
|
+
|
|
85
|
+
- 335041c: Stop declaring 13 `@object-ui/core` symbols under names `@objectstack/spec` owns
|
|
86
|
+
(objectui#3158, objectstack#4115 batch 4).
|
|
87
|
+
|
|
88
|
+
**Breaking for importers of `@object-ui/core`** — seven exported names changed,
|
|
89
|
+
because the spec exports the same name for a _different_ thing:
|
|
90
|
+
|
|
91
|
+
| was | now | what the spec's same-named export actually is |
|
|
92
|
+
| :----------------------- | :-------------------------------- | :--------------------------------------------------------------------------- |
|
|
93
|
+
| `ChartSeries` | `ChartSeriesBinding` | the authored dataset-binding descriptor (a measure `name`, no `data`) |
|
|
94
|
+
| `ActionHandler` | `ActionRunnerHandler` | the SERVER-side objectql handler, `(ctx) => unknown` |
|
|
95
|
+
| `PluginDefinition` | `RegistryPluginDefinition` | the platform PACKAGE manifest (`id`/`slug`/`staticPath`/install hooks) |
|
|
96
|
+
| `ValidationError` | `SchemaNodeValidationError` | plugin-manifest validation, keyed by `field`, no severity |
|
|
97
|
+
| `ValidationResult` | `SchemaNodeValidationResult` | ditto, with both arrays optional |
|
|
98
|
+
| `defineView` | `defineSystemView` | the VIEW-DOCUMENT factory: parses a `ViewSchema`, returns a validated `View` |
|
|
99
|
+
| `resolveCrudAffordances` | `resolveEffectiveCrudAffordances` | the object-level affordance matrix, with no notion of server API operations |
|
|
100
|
+
|
|
101
|
+
The other six keep their names and are now **imported from the spec** instead of
|
|
102
|
+
re-declared: `StyleMap`, `ResponsiveStyles` (ADR-0065), `RowHeight`,
|
|
103
|
+
`CONTEXT_TOKENS`, `CrudAffordances`, `RowCrudPredicates`.
|
|
104
|
+
|
|
105
|
+
**The copies were live misdescriptions, not just duplicates.** Three said so in
|
|
106
|
+
their own comments:
|
|
107
|
+
|
|
108
|
+
- `CONTEXT_TOKENS` carried a note that the duplication was "temporary until the
|
|
109
|
+
next coordinated release… because the installed `@objectstack/spec` predates
|
|
110
|
+
that export". The installed spec (17.0.0-rc.0) exports it, and the copy was
|
|
111
|
+
byte-identical — so it passed every value comparison and every behavioural
|
|
112
|
+
test for the whole interval in which its stated reason was false.
|
|
113
|
+
- `RowHeight` advertised itself as "the spec's `RowHeightSchema` vocabulary"
|
|
114
|
+
while being a hand-written union. It happened to be correct; nothing would
|
|
115
|
+
have caught the day it stopped being.
|
|
116
|
+
- `managedBy.ts` described itself as a "UI-side mirror of the framework's
|
|
117
|
+
`resolveCrudAffordances()`" and carried its own `DEFAULTS` table — a
|
|
118
|
+
line-for-line copy of the spec's `CRUD_AFFORDANCE_DEFAULTS`, plus a copy of
|
|
119
|
+
its override parser.
|
|
120
|
+
|
|
121
|
+
`resolveEffectiveCrudAffordances` now **delegates** the bucket/`userActions` half
|
|
122
|
+
to the spec's `resolveCrudAffordances()`, so the bucket table has exactly one
|
|
123
|
+
definition on the platform. What stays objectui's is the part the spec has no
|
|
124
|
+
notion of: intersecting that matrix with the server-resolved effective API
|
|
125
|
+
operation set (#3391), so the UI never offers a button the server would 405 —
|
|
126
|
+
and the name now says that instead of claiming to be the spec's function.
|
|
127
|
+
|
|
128
|
+
Deriving `RowCrudPredicates` also **tightens** it: the local copy typed
|
|
129
|
+
`visibleWhen`/`disabledWhen` as `unknown`, where the spec types them as
|
|
130
|
+
`Expression | ExpressionInput`. That was imprecision, not a deliberate dialect.
|
|
131
|
+
|
|
132
|
+
- b414983: fix(dashboard): a date globalFilter's preset-name default becomes a range, not an equality
|
|
133
|
+
|
|
134
|
+
Setup → System Overview rendered EVERY KPI tile as 0 while its period selector
|
|
135
|
+
read "All time" (objectstack#4475). Every request was `200 OK`, the widgets
|
|
136
|
+
rendered normally, and nothing in the UI signalled a failure — zeros read as
|
|
137
|
+
"nothing has happened yet" rather than as an error, which is why this survived
|
|
138
|
+
to an RC.
|
|
139
|
+
|
|
140
|
+
Both symptoms are one missing normalization. `resolveDashboardFilterDefs` lifts
|
|
141
|
+
the built-in `dateRange` declaration's preset NAME to `{ preset }`, but passed a
|
|
142
|
+
`globalFilters` entry's `defaultValue` through raw. `@objectstack/spec`'s
|
|
143
|
+
`GlobalFilterSchema.defaultValue` is `string | number | boolean`, so a bare
|
|
144
|
+
preset name is the ONLY spelling an author can write — and nothing ever mapped
|
|
145
|
+
it. System Overview declares
|
|
146
|
+
`{ field: 'created_at', type: 'date', defaultValue: 'last_7_days' }`, so:
|
|
147
|
+
|
|
148
|
+
- `buildFilterCondition` fell through to its "a bare string date means equality
|
|
149
|
+
on that day" branch and the widget sent
|
|
150
|
+
`runtimeFilter: { created_at: 'last_7_days' }`. The backend compiled
|
|
151
|
+
`SELECT COUNT(*) AS "user_count" FROM "sys_user" WHERE created_at = $1`
|
|
152
|
+
— verified against a live server, byte-for-byte the SQL in the issue. The
|
|
153
|
+
actual `sys_user` count is 4; that equality matches no row.
|
|
154
|
+
- `DateRangeFilter` derives its selected item from `value.preset` / `.from` /
|
|
155
|
+
`.to`, all `undefined` on a bare string, so the control fell through to its
|
|
156
|
+
ALL sentinel and displayed "All time" while sending that equality. The tiles
|
|
157
|
+
therefore looked deliberately unfiltered and merely empty.
|
|
158
|
+
|
|
159
|
+
`normalizeDateDefault` now applies the same lift the sibling `dateRange`
|
|
160
|
+
declaration already receives, for `date`/`dateRange` filters whose default names
|
|
161
|
+
a preset this module actually knows. This is not consumer-side leniency: it is
|
|
162
|
+
one normalization function completing the same conversion for the sibling
|
|
163
|
+
declaration, and the spec admits no other spelling for an author to fix at the
|
|
164
|
+
producer. A genuine ISO date string still means equality on that day (the
|
|
165
|
+
documented behaviour), and numbers, booleans and unrecognised strings are left
|
|
166
|
+
exactly as declared.
|
|
167
|
+
|
|
168
|
+
No backend change is needed: given a real range the dataset path already lowers
|
|
169
|
+
it correctly (`WHERE (created_at >= $1 AND created_at < $2)` → 4). The
|
|
170
|
+
framework's dashboard metadata needs none either — it is spec-compliant as
|
|
171
|
+
written, and editing it would only hide the defect.
|
|
172
|
+
|
|
173
|
+
Levelled `minor` rather than `patch` because the change is visible in rendered
|
|
174
|
+
dashboards rather than internal: any dashboard declaring a date-typed
|
|
175
|
+
`globalFilters` default now emits a different query shape, its numbers change
|
|
176
|
+
(from 0 to real values), and its filter control's displayed label changes with
|
|
177
|
+
them. Anything asserting on the previously-emitted condition will see it move.
|
|
178
|
+
|
|
179
|
+
Known residual, filed separately rather than widened into here: a `date` filter
|
|
180
|
+
whose value is neither a known preset nor a parseable ISO date still degrades
|
|
181
|
+
silently to an equality that matches nothing, producing the same
|
|
182
|
+
healthy-looking zero. Preset names are covered by this change; a misspelled
|
|
183
|
+
custom value is not.
|
|
184
|
+
|
|
185
|
+
- 256f8cc: fix(dashboard): an unrecognised date filter value is skipped and named, not compared
|
|
186
|
+
|
|
187
|
+
The residual the preset-name fix (objectui#3150 / objectstack#4475) left behind,
|
|
188
|
+
and the more deceptive half of it: a `date`/`dateRange` filter value that is
|
|
189
|
+
neither a known preset name nor a parseable date used to fall through to the
|
|
190
|
+
"a bare string date means equality on that day" branch. A misspelled default —
|
|
191
|
+
`defaultValue: 'last_7_dayz'` — therefore reached the widget query as
|
|
192
|
+
`runtimeFilter: { created_at: 'last_7_dayz' }`, which the backend faithfully
|
|
193
|
+
compiled to `WHERE created_at = $1`. `200 OK`, widget renders, count is 0 —
|
|
194
|
+
indistinguishable from "this range genuinely has no data". No 4xx, no console
|
|
195
|
+
warning, no UI signal. objectstack#4475 took a full RC cycle to catch for
|
|
196
|
+
exactly this reason: **0 looks like a legitimate answer**.
|
|
197
|
+
|
|
198
|
+
`buildFilterCondition` now holds a date value to three spellings, and only
|
|
199
|
+
three:
|
|
200
|
+
|
|
201
|
+
1. a known preset name → range bounds (unchanged, objectui#3150);
|
|
202
|
+
2. an ISO date (`2026-01-15`, `2026-01-15T08:30:00Z`) or a date-macro token
|
|
203
|
+
(`{today}`, `{7_days_ago}`) → equality on that day (the documented
|
|
204
|
+
behaviour, unchanged);
|
|
205
|
+
3. **anything else → the filter is skipped and `console.warn` names the filter,
|
|
206
|
+
the offending value, and the accepted spellings.**
|
|
207
|
+
|
|
208
|
+
The `{ preset: '<unknown>' }` object form gets the same voice. It already
|
|
209
|
+
dropped the filter — silently — because the preset lookup missed and no
|
|
210
|
+
`from`/`to` remained; that drop is now announced. When explicit bounds ride
|
|
211
|
+
along with an unknown preset the bounds are still honoured, and the warning says
|
|
212
|
+
which of the two won.
|
|
213
|
+
|
|
214
|
+
Rule 3 is deliberately the same strictness `buildWidgetScopedFilter` already
|
|
215
|
+
applies to a _default binding on a field the object does not have_ — skip and
|
|
216
|
+
warn, with the same rationale spelled out there: never emit a query the backend
|
|
217
|
+
can only empty-match. Field _names_ had that guard; field _values_ did not.
|
|
218
|
+
|
|
219
|
+
The macro-token check asks `resolveDateMacros` itself whether it recognises the
|
|
220
|
+
string, rather than restating its token grammar in a second place. One
|
|
221
|
+
vocabulary, no dialect to drift — and a token that resolver does not know
|
|
222
|
+
(`{last_7_dayz}`) is precisely the typo this guard exists to catch.
|
|
223
|
+
|
|
224
|
+
Levelled `minor`, matching objectui#3150, because the emitted query shape
|
|
225
|
+
changes: a dashboard carrying a misspelled date value stops sending a
|
|
226
|
+
never-matching equality and instead sends no constraint for that filter (its
|
|
227
|
+
numbers go from 0 to unfiltered) while the console says why. Anything asserting
|
|
228
|
+
on the previously-emitted equality will see it disappear.
|
|
229
|
+
|
|
230
|
+
Note the direction of the relaxation is chosen, not incidental: skipping widens
|
|
231
|
+
the result set, so the number visibly changes and the warning explains it —
|
|
232
|
+
whereas the old behaviour narrowed it to zero, which is the one outcome an
|
|
233
|
+
author cannot tell from a correct answer. Author-time rejection (validating
|
|
234
|
+
`GlobalFilterSchema.defaultValue` at publish, in `@objectstack/spec`) is the
|
|
235
|
+
stricter complement and belongs on the platform side; it is filed separately.
|
|
236
|
+
|
|
237
|
+
- d3584c6: Bring the whole `@objectstack` family to `17.0.0-rc.1`, so the dependency graph resolves a
|
|
238
|
+
single copy of `@objectstack/spec`.
|
|
239
|
+
|
|
240
|
+
#3178 bumped **only** `@objectstack/spec` to `17.0.0-rc.1`. The rest of the family —
|
|
241
|
+
`client`, `core`, `formula`, `lint` (and `sdui-parser`, reached through `lint`) — stayed on
|
|
242
|
+
`17.0.0-rc.0`, and each of them depends on spec at an **exact** version rather than a
|
|
243
|
+
caret:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
@objectstack/client@17.0.0-rc.0 -> spec "17.0.0-rc.0"
|
|
247
|
+
@objectstack/core@17.0.0-rc.0 -> spec "17.0.0-rc.0"
|
|
248
|
+
@objectstack/formula@17.0.0-rc.0 -> spec "17.0.0-rc.0"
|
|
249
|
+
@objectstack/lint@17.0.0-rc.0 -> spec "17.0.0-rc.0"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
So `main` carried **two** spec copies: objectui's own code read `17.0.0-rc.1` while every
|
|
253
|
+
`@objectstack/*` package read `17.0.0-rc.0` from its own nested `node_modules`. That breaks
|
|
254
|
+
the single-contract invariant this repo's guards are built on, and it breaks them
|
|
255
|
+
_silently_ — the affected checks depend on identity, not on version strings:
|
|
256
|
+
|
|
257
|
+
- `spec-subschema-parity.test.ts` distinguishes a genuine re-export from a fork by
|
|
258
|
+
**reference identity** of the zod schema object. Two spec copies make every schema a
|
|
259
|
+
distinct object, so a real re-export starts reading as a fork (or a fork slips through,
|
|
260
|
+
depending on which copy each side resolved).
|
|
261
|
+
- `scripts/check-spec-symbol-derivation.mjs` and `spec-symbol-parity.test.ts` use
|
|
262
|
+
`createRequire` to resolve spec's `.d.ts` and run it through the TS checker. With two
|
|
263
|
+
copies installed, _which_ declaration file the checker sees is a function of resolution
|
|
264
|
+
order rather than of intent.
|
|
265
|
+
|
|
266
|
+
The declared ranges were already `^17.0.0-rc.0`, which technically admits rc.1 — the pin
|
|
267
|
+
lived in the lockfile. Raising the remaining ranges to `^17.0.0-rc.1` makes the floor
|
|
268
|
+
explicit and forbids a future install from silently sliding back onto a family member that
|
|
269
|
+
drags rc.0 along with it. The rc.1 family members pin spec at `17.0.0-rc.1` exactly, so the
|
|
270
|
+
graph now converges on one copy by construction, not by luck.
|
|
271
|
+
|
|
272
|
+
No product behaviour changes here. `check:spec-symbols` reconciliation was already
|
|
273
|
+
completed by #3178 and stays green under the unified graph; this changeset is `minor`
|
|
274
|
+
per the repo's fixed-group version policy.
|
|
275
|
+
|
|
276
|
+
- 444457c: feat!: follow the framework's `managedBy: 'system'` → `'system-data'` retirement (objectstack#3355)
|
|
277
|
+
|
|
278
|
+
**FROM → TO: `managedBy: 'system'` → `managedBy: 'system-data'`.** The framework
|
|
279
|
+
retired the residual `system` bucket in protocol 17; this is the UI half of that
|
|
280
|
+
change, landing with it so the closed `ManagedByBucket` union stays a mirror
|
|
281
|
+
rather than a fork.
|
|
282
|
+
|
|
283
|
+
ADR-0103 split the overloaded `system` bucket additively in v16 — the
|
|
284
|
+
engine-owned objects moved to the explicit `engine-owned`, the admin/user-writable
|
|
285
|
+
ones stayed on `system` — which left that value named after the half that had
|
|
286
|
+
already moved out. `system-data` names what it actually holds: the SCHEMA is the
|
|
287
|
+
platform's, the DATA is the admin's or the user's.
|
|
288
|
+
|
|
289
|
+
**The derivation this deletes is the point.** Because v16's `system` doubled as
|
|
290
|
+
both the engine-owned default and the writable set, three UI surfaces had to
|
|
291
|
+
RECOVER the distinction from `userActions` at render time:
|
|
292
|
+
|
|
293
|
+
- `isSystemWritable()` probed `userActions` for any opted-in write. It is now
|
|
294
|
+
`managedBy === 'system-data'` — the bucket answers directly.
|
|
295
|
+
- `ManagedByBadge` derived a synthetic `'system-writable'` variant key. The
|
|
296
|
+
variant map is now 1:1 with the bucket union, so a new bucket is a compile
|
|
297
|
+
error to miss instead of a silent fallthrough. The `systemWritable` /
|
|
298
|
+
`system` i18n keys are **unchanged**, so no locale bundle moves.
|
|
299
|
+
- `resolveManagedByEmptyState()` asked the resolved `create` affordance whether a
|
|
300
|
+
`system` list should read "entries appear automatically" or show the New
|
|
301
|
+
button. `system-data` now falls through to the generic empty state by
|
|
302
|
+
definition; `engine-owned` keeps the automatic-entries copy.
|
|
303
|
+
|
|
304
|
+
**Breaking (UI API):** `ManagedByBadge`'s `userActions` prop and the exported
|
|
305
|
+
`ManagedByUserActions` interface are **removed**. The bucket alone selects the
|
|
306
|
+
variant now, so the prop had become metadata nothing read — the exact defect the
|
|
307
|
+
framework change exists to remove; shipping it as an accepted-but-ignored prop
|
|
308
|
+
would have reproduced it one layer up. Drop the prop from call sites; no other
|
|
309
|
+
change is needed.
|
|
310
|
+
|
|
311
|
+
`MANAGED_BY_BUCKETS` and `ManagedByBucket` no longer contain `'system'`.
|
|
312
|
+
|
|
313
|
+
- 850033c: Stop offering the retired `action.shortcut` / `action.bulkEnabled` keys.
|
|
314
|
+
|
|
315
|
+
`@objectstack/spec` 17 retired both as `retiredKey()` tombstones: authoring
|
|
316
|
+
either one is a hard PARSE REJECTION, so a draft carrying it cannot be saved
|
|
317
|
+
at all. The designer still offered controls for both — a "Bulk — apply to
|
|
318
|
+
multiple selected rows" checkbox and a "Shortcut" text field — which meant the
|
|
319
|
+
Studio action inspector let an author build a draft the platform would then
|
|
320
|
+
refuse, with the rejection arriving later and nowhere near the checkbox.
|
|
321
|
+
|
|
322
|
+
- **Action inspector**: both controls removed. The keys stay hidden from the
|
|
323
|
+
fallback form (the server's live schema still advertises them, so dropping
|
|
324
|
+
them from the hidden list would put the inputs straight back) — now under a
|
|
325
|
+
`RETIRED_FIELDS` list that says why, so nobody "restores the missing
|
|
326
|
+
control". `bulkEnabled`'s replacement is the list view's `bulkActions` /
|
|
327
|
+
`bulkActionDefs`; `shortcut` has none.
|
|
328
|
+
- **Action preview**: the `shortcut` and `bulk` pills are gone — they could
|
|
329
|
+
only ever render for metadata the platform now refuses.
|
|
330
|
+
- **`ActionEngine.registerActions`**: no longer harvests the two retired keys
|
|
331
|
+
from authored metadata, which made two dead registration options look
|
|
332
|
+
load-bearing. Both are still accepted on the single-action
|
|
333
|
+
`registerAction(action, options)` overload, where a HOST passes them
|
|
334
|
+
explicitly.
|
|
335
|
+
|
|
336
|
+
- 009e25d: Report / chart / query symbols stop wearing `@objectstack/spec`'s names
|
|
337
|
+
(objectui#3155, objectstack#4115).
|
|
338
|
+
|
|
339
|
+
**Breaking for TypeScript imports** — six exported names change. Each was a
|
|
340
|
+
different concept than the spec export it collided with, so an author reading
|
|
341
|
+
the objectui declaration as "the spec's" was reading a false claim:
|
|
342
|
+
|
|
343
|
+
| was | now | why they were never the same thing |
|
|
344
|
+
| :------------------ | :----------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
345
|
+
| `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 |
|
|
346
|
+
| `ChartSeriesSchema` | `ChartDataSeriesSchema` | zod twin of the above |
|
|
347
|
+
| `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`) |
|
|
348
|
+
| `QuerySchema` | `DriverQueryConfig` | ours is the high-level config `QueryASTBuilder` compiles; the spec exports that name as a zod schema value |
|
|
349
|
+
| `DriverInterface` | `SqlDriverInterface` | ours is objectui's SQL-oriented client abstraction (`query(sql, params)`); the spec's is the platform runtime driver contract |
|
|
350
|
+
| `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 |
|
|
351
|
+
|
|
352
|
+
Three more are now DERIVED from the spec instead of hand-restated, which fixes
|
|
353
|
+
live silent-stripping defects, since a `z.object()` drops unknown keys:
|
|
354
|
+
|
|
355
|
+
- **`DashboardWidgetSchema`** declared 10 of the spec's 22 keys, so
|
|
356
|
+
`objectui validate` deleted the other 12 without a word — `chartConfig`,
|
|
357
|
+
`colorVariant`, `filter`, `responsive`, `aria`,
|
|
358
|
+
`actionUrl`/`actionType`/`actionIcon`, `compareTo`, `suppressWarnings` and the
|
|
359
|
+
`requiresObject` / `requiresService` capability gates the dashboard renderer
|
|
360
|
+
honours at runtime. The TS interface had declared most of them all along, so a
|
|
361
|
+
widget could type-check and still lose half its configuration on validation.
|
|
362
|
+
Pinned divergences kept: `id` stays optional, `type` stays widened for the
|
|
363
|
+
objectui-only `list` / `custom` families, and the legacy `component` envelope
|
|
364
|
+
stays.
|
|
365
|
+
- **`GlobalFilterSchema`** took `scope` as a free-form string (any typo
|
|
366
|
+
validated); it now uses the spec's `widget | dashboard` vocabulary. The three
|
|
367
|
+
objectui widenings that back a real runtime normalizer are kept and pinned:
|
|
368
|
+
the bare-string `options` shorthand, the normalized `{ preset }` date default,
|
|
369
|
+
and an optional `optionsFrom.labelField`.
|
|
370
|
+
- **`AppContextSelectorSchema`** was a full restatement; spec keys and their
|
|
371
|
+
defaults now flow in by reference, with `label` widened for objectui's i18n
|
|
372
|
+
label envelope — which `AppContextSelectors` already renders.
|
|
373
|
+
|
|
374
|
+
`ListViewSchema`'s zod node now names the spec in its own initializer rather
|
|
375
|
+
than one hop away through a local const, so its long-standing derivation is
|
|
376
|
+
visible where it is declared.
|
|
377
|
+
|
|
378
|
+
Drift guard: `packages/types/src/__tests__/report-chart-query-spec-parity.test.ts`.
|
|
379
|
+
|
|
380
|
+
- 726b89c: `@object-ui/types` stops declaring sixteen symbols under names `@objectstack/spec` owns (objectui#3156, objectstack#4115).
|
|
381
|
+
|
|
382
|
+
Seven are now **derived** from the spec, nine are **renamed** to the local
|
|
383
|
+
dialect they always were. Both halves remove the same hazard: a local
|
|
384
|
+
declaration under a spec export's name reads as the spec's own definition to
|
|
385
|
+
the next reader, so a copy that is merely _correct today_ is a planted premise
|
|
386
|
+
tomorrow.
|
|
387
|
+
|
|
388
|
+
**Derived** — the spec now supplies the keys, by reference:
|
|
389
|
+
|
|
390
|
+
| symbol | derivation |
|
|
391
|
+
| :----------------------- | :-------------------------------------------------------------------------------- |
|
|
392
|
+
| `ActionParam` | `z.input<typeof ActionParamSchema>`, `type` widened to the local legacy spellings |
|
|
393
|
+
| `CreateExportJobRequest` | `Omit<CreateExportJobInput, 'object'>` (`object` is the method argument) |
|
|
394
|
+
| `CreateExportJobResult` | re-export from `@objectstack/spec/contracts` |
|
|
395
|
+
| `ImportRowResult` | re-export from `@objectstack/spec/api` |
|
|
396
|
+
| `NavigationArea` | spec keys, with `navigation` / `visible` pinned locally |
|
|
397
|
+
| `NavigationAreaSchema` | `specFieldsExcept(NavigationAreaSchema.shape, …)` |
|
|
398
|
+
| `Theme` | re-export of the spec's `ThemeInput` (the authoring shape) |
|
|
399
|
+
| `ExportJobFormat` | re-export of the spec's `ExportFormat` |
|
|
400
|
+
|
|
401
|
+
Four of these close real gaps rather than tidy names. `ActionParam` never
|
|
402
|
+
declared `reference` — the key `resolveActionParams()` actually reads for an
|
|
403
|
+
inline lookup target — nor `defaultFromRow`, which the metadata designer's own
|
|
404
|
+
inspector writes; it also narrowed `visible` to a bare string although the
|
|
405
|
+
resolver has always accepted the `{ dialect, source }` envelope too.
|
|
406
|
+
`CreateExportJobResult.createdAt` and `ImportRowResult.action` were optional
|
|
407
|
+
here and required by the server, leaving every consumer a branch that could
|
|
408
|
+
never run. And `NavigationArea`'s `id` now carries the spec's own length rule
|
|
409
|
+
instead of accepting any string.
|
|
410
|
+
|
|
411
|
+
**Renamed** — same word, different concept:
|
|
412
|
+
|
|
413
|
+
| was | now | why |
|
|
414
|
+
| :----------------- | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
|
|
415
|
+
| `FileMetadata` | `UploadedFileMetadata` | field-VALUE payload (`url`, `original_name`), not the storage file record |
|
|
416
|
+
| `GestureType` | `TouchGestureType` | direction-fused (`swipe-left`), not the spec's type+direction pair |
|
|
417
|
+
| `GestureConfig` | `TouchGestureConfig` | gesture→`action` binding, not per-gesture tuning |
|
|
418
|
+
| `OfflineConfig` | `PWAOfflineConfig` | service-worker route caching, not the offline data/sync model |
|
|
419
|
+
| `PageRegion` | `PageNodeRegion` | region of the renderer page NODE, holding `SchemaNode`s |
|
|
420
|
+
| `PageRegionSchema` | `PageNodeRegionSchema` | zod twin of the above |
|
|
421
|
+
| `ResponsiveConfig` | `MobileResponsiveConfig` | mobile box config, not the spec's SDUI grid contract |
|
|
422
|
+
| `WidgetManifest` | `RuntimeWidgetManifest` | SDUI component manifest, not the field-widget plugin manifest |
|
|
423
|
+
| `WidgetSource` | `RuntimeWidgetSource` | `module`/`inline`/`registry` loader union — and its `inline` carries a resolved component where the spec's carries source code |
|
|
424
|
+
|
|
425
|
+
**Migration**: the old names are gone, not deprecated — an alias would preserve
|
|
426
|
+
exactly the ambiguity being removed. Import the new name; nothing about the
|
|
427
|
+
shapes changed. `@object-ui/types` already re-exports the spec's own
|
|
428
|
+
`SpecResponsiveConfig`, and `@object-ui/react`'s `useOffline` config remains the
|
|
429
|
+
spec-shaped `OfflineConfig`, so both concepts stay reachable under
|
|
430
|
+
distinguishable names.
|
|
431
|
+
|
|
432
|
+
Each rename carries a bidirectional tripwire
|
|
433
|
+
(`packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts`): it fails if
|
|
434
|
+
the spec ever claims the new name, and also if the spec retires the old one —
|
|
435
|
+
at which point the natural name can be taken back rather than the workaround
|
|
436
|
+
outliving its reason.
|
|
437
|
+
|
|
438
|
+
### Patch Changes
|
|
439
|
+
|
|
440
|
+
- d9668a7: Honor the server's declared percent scale, so a ratio of exactly 1 renders as 100.0% (#3136)
|
|
441
|
+
|
|
442
|
+
A dataset measure declared `format: '0.0%'` rendered every ratio below 1
|
|
443
|
+
correctly and got the single most consequential one wrong: a rate of exactly
|
|
444
|
+
`1` printed as **`1.0%`**. On an SLA / pass-rate dashboard that turns
|
|
445
|
+
"everything met the SLA" into "1% met the SLA", on both surfaces the issue
|
|
446
|
+
names — the KPI card and the dataset-bound table (they share `formatMeasure`).
|
|
447
|
+
|
|
448
|
+
The cause was never a bad multiplier; it was a missing fact. `formatMeasure`
|
|
449
|
+
scaled by magnitude — `percentDisplayValue` multiplies by 100 only strictly
|
|
450
|
+
inside `(-1, 1)` — because the column arrived with a `%` format string and
|
|
451
|
+
nothing saying what scale its numbers were on. That guess is undecidable at
|
|
452
|
+
exactly 1, which is both a full-compliance ratio ("100%") and one percentage
|
|
453
|
+
point ("1%"), and it resolved to the reading almost nobody means.
|
|
454
|
+
|
|
455
|
+
The server now answers the question instead (framework: `percentScaleOf` +
|
|
456
|
+
`AnalyticsResult.fields[].percentScale`, the sibling of the ADR-0053 currency
|
|
457
|
+
chain): a `derived: { op: 'ratio' }` measure is a `fraction` by definition, and
|
|
458
|
+
a measure over a `percent` field inherits that field's scale. `formatMeasure`
|
|
459
|
+
takes the declared scale as a fourth argument and, when present, scales by it —
|
|
460
|
+
`fraction` ×100, `whole` verbatim — instead of inspecting the value. Every
|
|
461
|
+
dataset-bound call site passes the column's `percentScale`: the dashboard
|
|
462
|
+
metric/table/pivot cells, the report renderer's cells, totals and KPI, and the
|
|
463
|
+
dataset preview.
|
|
464
|
+
|
|
465
|
+
`percentDisplayValue` is untouched and still the fallback for a column that
|
|
466
|
+
arrives without the annotation (an older server, or a non-dataset percent cell
|
|
467
|
+
in a list view), so nothing that renders correctly today changes.
|
|
468
|
+
|
|
469
|
+
- a8ad6c0: A required boolean must be savable in its UNCHECKED state — `false` and `0` are values.
|
|
470
|
+
|
|
471
|
+
Reported against an AI-built task tracker whose 任务 object has a required
|
|
472
|
+
`是否完成` boolean: the create form showed the switch OFF, answered "是否完成不能
|
|
473
|
+
为空", and saved instantly once the switch was turned ON. The app could only ever
|
|
474
|
+
create ALREADY-DONE tasks — the one state the control shows by default was the
|
|
475
|
+
one value it refused to save (cloud#972).
|
|
476
|
+
|
|
477
|
+
Two defects stacked, and either alone is enough to break it:
|
|
478
|
+
|
|
479
|
+
**The `required` verdict read truthiness, not presence.** `@objectstack/spec`
|
|
480
|
+
FieldSchema.required (ADR-0113) is "an insert must provide a NON-NULL value",
|
|
481
|
+
and objectql's record validator implements exactly that. react-hook-form's
|
|
482
|
+
built-in rule instead fails whenever `isBoolean(value) && !value` — its
|
|
483
|
+
accept-the-terms checkbox heritage — silently redefining every required boolean
|
|
484
|
+
as "must be TRUE", including a select whose chosen option value is `false`. It
|
|
485
|
+
also disagreed the other way, letting a whitespace-only string through for the
|
|
486
|
+
server to reject with a 400. The form renderer no longer hands RHF its own
|
|
487
|
+
`required`: the check is now a `validate` entry keyed `required` (so the error
|
|
488
|
+
still surfaces as `type: 'required'`, which the conditional-required cleanup
|
|
489
|
+
keys on) backed by a new shared `isMissingForRequired` in `@object-ui/core`, a
|
|
490
|
+
deliberate mirror of objectql `record-validator.isMissing` — `undefined`,
|
|
491
|
+
`null`, blank-after-trim string, empty array. Deleting the inherited rule also
|
|
492
|
+
stops a `required` that rode in on `validation` from outliving a `requiredWhen`
|
|
493
|
+
that resolved to FALSE.
|
|
494
|
+
|
|
495
|
+
**A boolean field held `undefined` while displaying "off".** A two-state control
|
|
496
|
+
has no third state, but a field with no entry in `defaultValues` rendered an OFF
|
|
497
|
+
switch backed by nothing: the create payload omitted the column (it lands null,
|
|
498
|
+
which reads as unchecked but isn't) and the presence check above would still
|
|
499
|
+
refuse it. The form renderer now folds `false` into `defaultValues` for every
|
|
500
|
+
boolean-widget field the caller left unset — in `defaultValues` itself, not
|
|
501
|
+
per-Controller, because that object is also the dirty-check baseline and what
|
|
502
|
+
the defaults-reset window replays. Every surface gets it, including the
|
|
503
|
+
modal/drawer create dialogs that start from a bare `{}`. An authored default
|
|
504
|
+
(or a loaded record, `null` included) still wins.
|
|
505
|
+
|
|
506
|
+
`WizardForm`'s cross-step gate had its own copy of the empty-value predicate; it
|
|
507
|
+
now imports the shared one so it cannot drift from the per-field verdict. And
|
|
508
|
+
the field-demo renderer read `schema.defaultValue || schema.value`, throwing
|
|
509
|
+
away an authored default of `false` / `0` / `''` — same falsy-as-empty class,
|
|
510
|
+
now `??`.
|
|
511
|
+
|
|
512
|
+
Verified end to end on a local stack against the exact metadata shape
|
|
513
|
+
`apply_blueprint` materializes (`{ type: 'boolean', required: true }`, no
|
|
514
|
+
default): a 是否完成 = 否 task with 工时 = 0 now creates and persists as
|
|
515
|
+
`{ hours: 0, is_done: false }`, turning the switch on still stores `true`, and a
|
|
516
|
+
blank required text is still refused.
|
|
517
|
+
|
|
518
|
+
- Updated dependencies [4ae0ac4]
|
|
519
|
+
- Updated dependencies [696e3c1]
|
|
520
|
+
- Updated dependencies [4bf612c]
|
|
521
|
+
- Updated dependencies [cb82705]
|
|
522
|
+
- Updated dependencies [f572849]
|
|
523
|
+
- Updated dependencies [444457c]
|
|
524
|
+
- Updated dependencies [022e4c3]
|
|
525
|
+
- Updated dependencies [009e25d]
|
|
526
|
+
- Updated dependencies [726b89c]
|
|
527
|
+
- @object-ui/types@17.2.0
|
|
528
|
+
|
|
529
|
+
## 17.1.0
|
|
530
|
+
|
|
531
|
+
### Minor Changes
|
|
532
|
+
|
|
533
|
+
- 62311b6: feat(core): inventory `ActionDef`'s keys and warn on the ones nothing reads — objectstack#4075 step 1
|
|
534
|
+
|
|
535
|
+
`ActionDef` ends with `[key: string]: any`, so it accepts any key of any type.
|
|
536
|
+
Deleting `ActionDef.execute` produced **zero** compile errors even though the
|
|
537
|
+
field had just been removed (objectui#2990), and stale metadata still authoring
|
|
538
|
+
`execute: 'markDone'` type-checks today. The same deletion against
|
|
539
|
+
`@object-ui/types`' `ActionSchema` — which has no index signature — correctly
|
|
540
|
+
produced `TS2353` at the authoring site. One of the two readers can catch a
|
|
541
|
+
retired key; the other is structurally incapable, which is how a typo (`targt`)
|
|
542
|
+
and a tombstoned key both reach a runner that then silently does nothing.
|
|
543
|
+
|
|
544
|
+
This is the non-breaking first step of the staged narrowing: it makes the key set
|
|
545
|
+
**visible** and warns on anything outside it, without changing a single type.
|
|
546
|
+
|
|
547
|
+
New exports from `@object-ui/core`:
|
|
548
|
+
|
|
549
|
+
- `ACTION_DEF_KEYS`, `SPEC_ACTION_KEYS`, `NAVIGATION_ALIAS_KEYS`,
|
|
550
|
+
`RETIRED_ACTION_KEYS`, `KNOWN_ACTION_KEYS` — the inventory.
|
|
551
|
+
- `classifyActionKeys(action)` — splits an action's own keys into `unknown` and
|
|
552
|
+
`retired`.
|
|
553
|
+
- `warnOnUnknownActionKeys(action)` — dev-mode only, warn-once. Called by
|
|
554
|
+
`ActionRunner.execute`, so no consumer wiring is needed.
|
|
555
|
+
|
|
556
|
+
A retired key gets a louder, more specific warning than an unknown one: an
|
|
557
|
+
unknown key is probably a typo, a retired key is metadata that used to work.
|
|
558
|
+
`execute` is not simply gone from the spec — it is a live **tombstone**, still
|
|
559
|
+
present in `ActionSchema` so the parser can reject it by name with the rename
|
|
560
|
+
prescription.
|
|
561
|
+
|
|
562
|
+
Nothing is rejected and no types changed, so existing metadata behaves exactly as
|
|
563
|
+
before. Promoting the legitimate keys to explicit optional fields, then removing
|
|
564
|
+
the index signature so `tsc` catches both typos and retired keys, are steps 2 and
|
|
565
|
+
3 of objectstack#4075.
|
|
566
|
+
|
|
567
|
+
- 9e7349e: **`target` is the only action handler slot — the `execute` alias is gone from the renderer (framework#3856).**
|
|
568
|
+
|
|
569
|
+
`ActionRunner.executeScript` read `action.target || action.execute`. That fallback
|
|
570
|
+
is unreachable against `@objectstack/spec` 17: `execute` is now a tombstoned key
|
|
571
|
+
(framework#3855) that the parser **rejects** with the rename prescription, so no
|
|
572
|
+
parsed action can carry it and the `||` could only ever yield `target`. Verified
|
|
573
|
+
against 17.0.0-rc.0 — an action declaring `execute` fails `ActionSchema.safeParse`,
|
|
574
|
+
and a `target` action's parsed output has no `execute` key at all.
|
|
575
|
+
|
|
576
|
+
Deleted rather than left as harmless residue: two handler slots is what let one
|
|
577
|
+
action run one script server-side and a different one client-side (framework#3713,
|
|
578
|
+
where this renderer preferred the alias while the spec transform preferred
|
|
579
|
+
`target`). A dead slot still reads as a live contract to the next maintainer.
|
|
580
|
+
|
|
581
|
+
`execute` is also **removed from the types**, which is the part that had never
|
|
582
|
+
landed. framework#3856 predicted a compile error here; there wasn't one, because
|
|
583
|
+
neither reader was typed against the spec's `z.infer`:
|
|
584
|
+
|
|
585
|
+
- `@object-ui/types` `ActionSchema` hand-declared `execute?: string`. Removed, so
|
|
586
|
+
`execute: '…'` now fails `tsc` at the authoring site (TS2353).
|
|
587
|
+
- `@object-ui/core` `ActionDef` hand-declared it too. Removed — but `ActionDef`
|
|
588
|
+
carries a `[key: string]: any` index signature, so stale hand-authored metadata
|
|
589
|
+
that never passed through the parser still compiles. For that path
|
|
590
|
+
`executeScript` now returns the rename prescription instead of a bare
|
|
591
|
+
"No script provided", matching the spec tombstone's rule that removing an
|
|
592
|
+
authorable key must be audible: silently binding no handler is the
|
|
593
|
+
"Mark Done does nothing" shape (framework#2169).
|
|
594
|
+
|
|
595
|
+
The four action renderers (`action:button`, `action:icon`, `action:menu`,
|
|
596
|
+
`action:group`) no longer forward `execute` into the runner, and Studio's
|
|
597
|
+
`ActionPreview` no longer falls back to it — previewing an alias-only draft as
|
|
598
|
+
"bound" contradicted the parse that rejects it on save.
|
|
599
|
+
|
|
600
|
+
Requires `@objectstack/spec` 17. Metadata still on the alias is rewritten by
|
|
601
|
+
`os migrate meta --from 16`.
|
|
602
|
+
|
|
603
|
+
- 6ae818e: feat(core): one column identity per column — `field` stamped at ingestion (#3104)
|
|
604
|
+
|
|
605
|
+
A column's field identity was resolved twice, with two different precedences
|
|
606
|
+
over the same `schema.columns` array, and the two halves disagreed:
|
|
607
|
+
|
|
608
|
+
- **request path** — `ListView`'s `$expand` and `$select` builders, and
|
|
609
|
+
`ObjectGrid.getSelectFields`, read `f?.field` and only `f?.field`.
|
|
610
|
+
- **render path** — the FLS gate, the hidden-field filter, `fieldOrder`, both
|
|
611
|
+
export branches and the hide-fields popover read
|
|
612
|
+
`f.name || f.fieldName || f.field` — name FIRST.
|
|
613
|
+
|
|
614
|
+
So `{ field: 'account', name: 'account_name' }` fetched `account` while the
|
|
615
|
+
renderer keyed off `account_name`, and `{ name: 'account' }` rendered a column
|
|
616
|
+
the request dropped entirely — neither `$select` nor `$expand` carried it. That
|
|
617
|
+
is the mechanism behind the "relation column shows a bare id / column is empty
|
|
618
|
+
/ sort does nothing / export is missing a column" defect class.
|
|
619
|
+
|
|
620
|
+
Per AGENTS.md #0.1 the fix is not another `?? name` at the read sites. Legacy
|
|
621
|
+
acceptance moves to the one boundary that already folds this view's vocabulary,
|
|
622
|
+
`normalizeListViewSchema`, which now also canonicalizes each column's identity.
|
|
623
|
+
|
|
624
|
+
New in `@object-ui/core`:
|
|
625
|
+
|
|
626
|
+
- `columnIdentity(entry)` — the single reader. Resolves `field` → `name` →
|
|
627
|
+
`fieldName`, canonical-first, so it agrees with `buildExpandFields` instead
|
|
628
|
+
of racing it. Handles bare-string columns.
|
|
629
|
+
- `normalizeColumnIdentity(entry)` / `normalizeColumnIdentities(columns)` — the
|
|
630
|
+
fold. Stamps `field`; a legacy key that is **already present** is mirrored
|
|
631
|
+
onto the same identity so name-first readers resolve what the request asked
|
|
632
|
+
for; a legacy key that is **absent is never invented**, and an
|
|
633
|
+
already-canonical column is returned by reference.
|
|
634
|
+
- `hasConflictingColumnIdentity(entry)` — true when a column's keys disagree.
|
|
635
|
+
- `CANONICAL_COLUMN_IDENTITY_KEY`, `LEGACY_COLUMN_IDENTITY_KEYS`,
|
|
636
|
+
`TABLE_ADAPTER_COLUMN_KEY`.
|
|
637
|
+
|
|
638
|
+
The fold **mirrors** rather than deleting the legacy key, unlike the other
|
|
639
|
+
folds in `normalizeListViewSchema`. Deleting would work inside this repo (every
|
|
640
|
+
name-first read falls through to `field`), but `columns` entries cross the
|
|
641
|
+
package boundary into host renderers and dropping `name` from under them is a
|
|
642
|
+
breaking change with no inventory. Deletion is a later call, once the in-repo
|
|
643
|
+
consumers read `columnIdentity()`.
|
|
644
|
+
|
|
645
|
+
Behaviour is unchanged for any column carrying a single identity key — every
|
|
646
|
+
read site resolves the same string it did before. The entries whose resolution
|
|
647
|
+
moves are exactly the ones where two sites already disagreed.
|
|
648
|
+
|
|
649
|
+
`accessorKey` is deliberately untouched: it is TanStack Table's own column key
|
|
650
|
+
(`TableColumn.accessorKey`), not ObjectStack metadata identity, and folding
|
|
651
|
+
across that boundary would fossilize the merge.
|
|
652
|
+
|
|
653
|
+
- 746dd00: feat(sdui): curate the page:\*, element:\* and action:\* families into the public contract
|
|
654
|
+
|
|
655
|
+
The AI-authoring vocabulary and the Studio page designer disagreed by thirteen
|
|
656
|
+
blocks: `PUBLIC_BLOCKS` carried one `page:` tag and one `element:` tag while
|
|
657
|
+
the designer palette — and @objectstack/spec's page schema — offered the whole
|
|
658
|
+
families. A block a human can drag in Studio was invisible to a model writing
|
|
659
|
+
the same page, which is the objectui#3006 state at 10× the scale.
|
|
660
|
+
|
|
661
|
+
Fifteen tags join the contract (36 → 42 → **57**), every one shipping a
|
|
662
|
+
renderer with declared inputs (objectui#3065):
|
|
663
|
+
|
|
664
|
+
- `page:` — tabs, card, accordion, section, footer, sidebar
|
|
665
|
+
- `element:` — text, number, button, definition-list, repeater
|
|
666
|
+
- `action:` — button, group, menu, icon
|
|
667
|
+
|
|
668
|
+
Five stay out, each with its reason recorded and guarded: `action:bar`
|
|
669
|
+
(`record:quick_actions` covers the record action strip; the spec blesses the
|
|
670
|
+
other four), `element:image` (duplicates the curated `image` — one spelling
|
|
671
|
+
per concept), and `element:record_picker` / `element:text_input` /
|
|
672
|
+
`element:metadata_viewer` (mirroring the Studio palette's own exclusions, so
|
|
673
|
+
the two vocabularies stay out for the same reasons rather than by
|
|
674
|
+
coincidence).
|
|
675
|
+
|
|
676
|
+
The console's reverse-coverage guard now sweeps all four semantic namespaces
|
|
677
|
+
instead of `record:` alone — checking only the namespace you just fixed is
|
|
678
|
+
exactly how the last 22 doubled keys went unnoticed (objectui#3037). A new
|
|
679
|
+
prop-less allowlist (`element:divider`, `page:section`, `page:footer`,
|
|
680
|
+
`page:sidebar`) keeps "declares no inputs" a pinned decision in both
|
|
681
|
+
directions: those four must stay at zero, everything else curated must declare
|
|
682
|
+
a surface.
|
|
683
|
+
|
|
684
|
+
- 38ca8be: refactor(fields): `requiredWhen` is the only required-predicate slot — drop the retired `conditionalRequired` alias
|
|
685
|
+
|
|
686
|
+
`@objectstack/spec` 17 (objectstack#3855) **retired** `Field.conditionalRequired`,
|
|
687
|
+
the long-deprecated alias of `requiredWhen`. ObjectUI carried a back-compat read
|
|
688
|
+
for it in seven places; all of them are removed.
|
|
689
|
+
|
|
690
|
+
The removal is safe because the spec did not merely _stop emitting_ the key — it
|
|
691
|
+
made authoring it **fail loudly**. `retiredKey()` declares the key as
|
|
692
|
+
`z.never()`, so:
|
|
693
|
+
|
|
694
|
+
- `z.input` types it as `never` — writing it is a `tsc` error at the authoring site;
|
|
695
|
+
- the parse **rejects** it (verified against `17.0.0-rc.0`), at both `FieldSchema`
|
|
696
|
+
and `ObjectSchema`, with the prescription as the message:
|
|
697
|
+
|
|
698
|
+
> `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use
|
|
699
|
+
> `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged.
|
|
700
|
+
> Run `os migrate meta --from 16` to rewrite it automatically.
|
|
701
|
+
|
|
702
|
+
So spec-parsed metadata cannot carry the key — an object declaring it fails to
|
|
703
|
+
load rather than loading with the rule silently dropped. Keeping a renderer-side
|
|
704
|
+
`requiredWhen ?? conditionalRequired` would have re-created exactly the second
|
|
705
|
+
de-facto contract the tombstone exists to prevent: the key would have kept
|
|
706
|
+
working in the UI while being rejected everywhere else, hiding the producer's bug
|
|
707
|
+
(AGENTS.md #0.1). "Backend-agnostic" (#1) does not argue for keeping it either —
|
|
708
|
+
`conditionalRequired` is an ObjectStack-spec-ism, so the only producers that ever
|
|
709
|
+
emit it are ObjectStack producers on ≤16, and the spec ships them a converter.
|
|
710
|
+
|
|
711
|
+
Removed from:
|
|
712
|
+
|
|
713
|
+
| package | site |
|
|
714
|
+
| :----------------------- | :-------------------------------------------------------------------------------------------------------- |
|
|
715
|
+
| `@object-ui/types` | the `conditionalRequired?:` member on `FormField` |
|
|
716
|
+
| `@object-ui/core` | the `??` fallback + rules-param member in `resolveFieldRuleState` |
|
|
717
|
+
| `@object-ui/components` | three pass-throughs in the form renderer |
|
|
718
|
+
| `@object-ui/plugin-form` | `ObjectForm`, `ModalForm`, `sectionFields`, `deriveMasterDetail` (×2) |
|
|
719
|
+
| `@object-ui/app-shell` | the field inspector's legacy read/auto-migrate, and the key's entry in `clientValidation`'s CEL lint list |
|
|
720
|
+
|
|
721
|
+
**Studio authors lose nothing.** The object designer's draft validation parses
|
|
722
|
+
against the spec's own `ObjectSchema`, so a draft carrying the key now surfaces
|
|
723
|
+
the tombstone's rename prescription under the same `fields.<name>.conditionalRequired`
|
|
724
|
+
path the CEL lint used to report — a better message than the inspector's silent
|
|
725
|
+
auto-migration, and one the server agrees with. That behavior is pinned by a test.
|
|
726
|
+
|
|
727
|
+
**Migrating:** rename the key to `requiredWhen` (the CEL value is unchanged), or
|
|
728
|
+
run `os migrate meta --from 16`.
|
|
729
|
+
|
|
730
|
+
- 02aef0c: fix(sdui): a `kind:'html'` page can use lazily-registered blocks, and recovers when one registers late
|
|
731
|
+
|
|
732
|
+
objectui#2953 had a twin one tier over, unreported. The whitelist a
|
|
733
|
+
`kind:'html'` page's source compiles against was built from `getAllTypes()` +
|
|
734
|
+
`getConfig()` — both loaded-only — so any block registered via `registerLazy()`
|
|
735
|
+
was rejected as _"not an allowed component"_.
|
|
736
|
+
|
|
737
|
+
The blast radius is worse than the react tier's. There, a missing block cost one
|
|
738
|
+
identifier; here a compile diagnostic fails the **whole page**, so a single
|
|
739
|
+
`<object-kanban>` replaced the entire page with `HTML page failed to compile (2)`.
|
|
740
|
+
And it never recovered: `layoutElement` was memoised on `[schema, pageType]` with
|
|
741
|
+
no registry signal, so the cached error panel outlived the plugin actually
|
|
742
|
+
landing — permanently broken for the session.
|
|
743
|
+
|
|
744
|
+
`ComponentRegistry` gains three lazy-aware reads:
|
|
745
|
+
|
|
746
|
+
- `getKnownTypes()` — loaded registrations **plus** pending lazy stubs, deduped.
|
|
747
|
+
The set a whitelist or manifest should be built from. `getAllTypes()` keeps its
|
|
748
|
+
loaded-only meaning ("what can render right now") and now says so.
|
|
749
|
+
- `getMeta(type, namespace?)` — metadata from the loaded registration, else from
|
|
750
|
+
a pending stub. `getConfig()` stays loaded-only, since callers read
|
|
751
|
+
`.component` off it.
|
|
752
|
+
- `getVersion()` — monotonic counter of changes to the known set, bumped on
|
|
753
|
+
register / unregister / registerLazy. A cache key that a type _count_ cannot
|
|
754
|
+
substitute for: one registration plus one unregistration leaves the count
|
|
755
|
+
untouched while the set changed.
|
|
756
|
+
|
|
757
|
+
`getJsxManifest()` builds from those, and `PageRenderer` subscribes to the
|
|
758
|
+
registry so a page that could not compile retries when the registry grows.
|
|
759
|
+
|
|
760
|
+
A stub carries no `inputs` yet, so its props surface as `unknown-prop` warnings
|
|
761
|
+
rather than errors — the page compiles and renders, and the inner
|
|
762
|
+
`SchemaRenderer` triggers the loader and swaps in the real block. Authoring-time
|
|
763
|
+
prop validation is unaffected: `sdui.manifest.json` is generated with every
|
|
764
|
+
plugin eagerly loaded, and asserts as much.
|
|
765
|
+
|
|
766
|
+
- c4db402: refactor(views): ListView's `aria` and `sharing` are the spec sub-shapes (#2890 scope A step 5)
|
|
767
|
+
|
|
768
|
+
Last rename batch in the ListView vocabulary migration.
|
|
769
|
+
|
|
770
|
+
**`aria`** is now the spec's `AriaPropsSchema`: `label` → `ariaLabel`,
|
|
771
|
+
`describedBy` → `ariaDescribedBy`, folded at the ListView boundary like every
|
|
772
|
+
other legacy key. Two things fall out of adopting the spec shape:
|
|
773
|
+
|
|
774
|
+
- `role` becomes authorable. The list region hardcoded `role="region"`; it now
|
|
775
|
+
reads `aria.role` and falls back to `region`.
|
|
776
|
+
- `aria.live` stays as a documented local extension — it has no spec
|
|
777
|
+
counterpart, and dropping it would silently disable a shipped capability.
|
|
778
|
+
Promote it rather than growing that extension.
|
|
779
|
+
|
|
780
|
+
**`sharing`** is now the spec's `ViewSharingSchema` (`{ type, lockedBy }`),
|
|
781
|
+
imported by reference — the local four-key object is gone. The legacy pair folds
|
|
782
|
+
in: `visibility` collapses onto the two ownership kinds the spec models (only
|
|
783
|
+
`private` is `personal`; `team` / `organization` / `public` are all
|
|
784
|
+
`collaborative`), and a bare `enabled: true` maps to `personal`, which is the
|
|
785
|
+
badge the user already saw (the old title fell back to `'private'`).
|
|
786
|
+
|
|
787
|
+
_Visible change_: the share badge's tooltip shows the spec ownership type, so a
|
|
788
|
+
view authored with `visibility: 'team'` reads "Sharing: collaborative" instead
|
|
789
|
+
of "Sharing: team". The four-value audience has no spec home and nothing but
|
|
790
|
+
that tooltip consumed it; keeping a second audience enum alive would re-open the
|
|
791
|
+
fork this issue closes.
|
|
792
|
+
|
|
793
|
+
Also fixes the **spec bridge**, which was doing the opposite of its job: given a
|
|
794
|
+
spec-shaped `sharing`, `transformListView` _downgraded_ it — inventing a legacy
|
|
795
|
+
`visibility` audience and an `enabled` flag that the renderer then had to fold
|
|
796
|
+
back. Both sides speak `ViewSharing` now, so it passes through.
|
|
797
|
+
|
|
798
|
+
`conditionalFormatting` and `exportOptions` are deliberately **not** folded.
|
|
799
|
+
Both objectui shapes are supersets carrying capability the spec cannot express —
|
|
800
|
+
the `{ field, operator, value }` rule form, and `maxRecords` / `includeHeaders`
|
|
801
|
+
/ `fileNamePrefix`. Folding them onto the narrower spec shapes would delete
|
|
802
|
+
working features; they want promotion upstream, not a rename.
|
|
803
|
+
|
|
804
|
+
- 5319bf1: feat(views): the list toolbar speaks one vocabulary — `userActions` (#2890 scope A step 3)
|
|
805
|
+
|
|
806
|
+
The seven bare `show*` toolbar flags fold into the spec's `userActions`, and the
|
|
807
|
+
renderer reads nothing else. `showDescription` folds into
|
|
808
|
+
`appearance.showDescription` at the same boundary.
|
|
809
|
+
|
|
810
|
+
| legacy | canonical |
|
|
811
|
+
| :-------------------------------------------------------- | :-------------------------------------------------------- |
|
|
812
|
+
| `showSearch` / `showSort` / `showFilters` / `showDensity` | `userActions.search` / `.sort` / `.filter` / `.rowHeight` |
|
|
813
|
+
| `showGroup` / `showHideFields` / `showColor` | `userActions.group` / `.hideFields` / `.rowColor` |
|
|
814
|
+
| `showDescription` | `appearance.showDescription` |
|
|
815
|
+
|
|
816
|
+
**The last three are new keys, and they close a capability hole rather than just
|
|
817
|
+
renaming one.** `@objectstack/spec`'s `UserActionsConfigSchema` documents itself
|
|
818
|
+
as "which interactive actions are available to users in the view toolbar — each
|
|
819
|
+
boolean toggles the corresponding toolbar element on/off", and already carries
|
|
820
|
+
`rowHeight` (objectui's `showDensity` under its spec name). Grouping, column
|
|
821
|
+
visibility and row coloring are the same kind of toggle: the spec models all
|
|
822
|
+
three as _configuration_ (`grouping`, `hiddenFields`, `rowColor`) but has no
|
|
823
|
+
"may the user change it" switch for any of them.
|
|
824
|
+
|
|
825
|
+
The consequence was visible in the product. With no `userActions` key to read,
|
|
826
|
+
the two list surfaces **hardcoded opposite policies**: `InterfaceListPage` (the
|
|
827
|
+
author-curated interface page) pinned all three OFF, `ObjectDataPage` pinned two
|
|
828
|
+
ON — and an interface-page author could not turn grouping back on for end users
|
|
829
|
+
at all. Both surfaces now express their policy as `userActions` defaults, which
|
|
830
|
+
an author can override.
|
|
831
|
+
|
|
832
|
+
Until the keys land in `@objectstack/spec`, `@object-ui/types` carries them as a
|
|
833
|
+
documented `.extend()` on `UserActionsConfigSchema` (the same shape
|
|
834
|
+
`ListColumnSchema` uses while waiting on objectstack#3761); it collapses into a
|
|
835
|
+
plain re-export once they do. Note the spec schema is not `.strict()`, so before
|
|
836
|
+
this an author writing `userActions: { group: false }` had it **silently
|
|
837
|
+
stripped** — valid on parse, no effect at render.
|
|
838
|
+
|
|
839
|
+
Defaults are unchanged and deliberately asymmetric, matching what these flags
|
|
840
|
+
have always done: `search` / `sort` / `filter` / `rowHeight` / `group` are on
|
|
841
|
+
unless turned off; `hideFields` / `rowColor` are off unless turned on. Making
|
|
842
|
+
them uniform would grow two buttons on every existing view, so it is left as its
|
|
843
|
+
own product decision rather than smuggled into a vocabulary migration.
|
|
844
|
+
|
|
845
|
+
Also drops a dead relay in app-shell's `ObjectView`, which forwarded
|
|
846
|
+
`showDescription` onto the node although `ListView` has only ever read
|
|
847
|
+
`appearance.showDescription`.
|
|
848
|
+
|
|
849
|
+
- f59f2c1: refactor(actions): `navigation` becomes a named alias of the spec's `url`, sharing one navigator (#2944)
|
|
850
|
+
|
|
851
|
+
The last open item of #2944: `ActionRunner` dispatched a seventh action type,
|
|
852
|
+
`navigation`, that `@objectstack/spec`'s `ActionType` does not contain. The issue
|
|
853
|
+
asked for a decision — promote it upstream or delete the case. Neither, as stated.
|
|
854
|
+
|
|
855
|
+
- **Promoting it is wrong.** The spec already has `url` for "go to a location",
|
|
856
|
+
with `openIn` for the new-tab/same-tab choice. A seventh type would put a
|
|
857
|
+
second spec name on one operation, which is the exact failure the #2901 audit
|
|
858
|
+
is named after: _a second definition of the vocabulary exists, and the renderer
|
|
859
|
+
is faithful to the wrong one_.
|
|
860
|
+
- **Deleting it is worse, because it is silent.** `{ type: 'navigation', to: … }`
|
|
861
|
+
is authored today (`element:button` CTAs). Without the case the action falls
|
|
862
|
+
through to `executeActionSchema`, which returns `{ success: true }` — a green
|
|
863
|
+
toast that navigates nowhere. That is #2960's trap.
|
|
864
|
+
|
|
865
|
+
So it stays, but stops being dialect. `ObjectUiLocalActionType` /
|
|
866
|
+
`OBJECTUI_LOCAL_ACTION_TYPES` in `@object-ui/types` declare it as objectui's own
|
|
867
|
+
alias of `url` — the same treatment #2985 gave `PageVisualizationAlias` — and the
|
|
868
|
+
runner routes both names through one navigator.
|
|
869
|
+
|
|
870
|
+
**The alias had already drifted, which is the point.** `executeNavigation` was
|
|
871
|
+
quietly the weaker of the two implementations: no `${param.X}` / `${ctx.X}`
|
|
872
|
+
interpolation, `openIn` ignored, and no `/api/…` full-page short-circuit (the
|
|
873
|
+
redirect-dance case `url` exists to handle). An author who wrote
|
|
874
|
+
`{ type: 'navigation', to: '/x?p=${param.p}' }` shipped the literal `${param.p}`,
|
|
875
|
+
while the identical `url` action resolved it. Both names now behave identically;
|
|
876
|
+
`url` in turn gains `replace` pass-through, the one modifier only the alias had.
|
|
877
|
+
|
|
878
|
+
Additive only. `replace` is omitted from the `NavigationHandler` options object
|
|
879
|
+
when unset, so hosts see the option shape they already saw.
|
|
880
|
+
|
|
881
|
+
The new guard is structural rather than another assertion. The runner's built-in
|
|
882
|
+
dispatch is a table typed `Record<RunnableActionType, …>` instead of a `switch`,
|
|
883
|
+
so an `ActionType` the spec **adds** stops compiling until an executor exists for
|
|
884
|
+
it — the Tier-2 "validates at save, renders nothing at run time" failure (#2942)
|
|
885
|
+
becomes a build error for actions. `spec-derived-unions.test.ts` additionally
|
|
886
|
+
asserts `navigation` is _absent_ from the spec enum, so the day it is adopted
|
|
887
|
+
upstream, the test fails and names the alias to retire.
|
|
888
|
+
|
|
889
|
+
- ce08d55: chore(deps): upgrade `@objectstack/*` to 17.0.0-rc.0, and let the spec take back what it now owns
|
|
890
|
+
|
|
891
|
+
`spec` / `client` / `formula` / `lint` move from `^16.x` to `^17.0.0-rc.0`. Two
|
|
892
|
+
groups of v17 changes reach this repo, and they pull in opposite directions —
|
|
893
|
+
the spec pruned surface objectui re-exported, and adopted surface objectui had
|
|
894
|
+
been carrying locally.
|
|
895
|
+
|
|
896
|
+
**The spec pruned dead Theme config (objectstack#3494), so the re-exports went
|
|
897
|
+
with it.** `ThemeSchema` dropped `spacing`, `breakpoints`, `logo`, `density`,
|
|
898
|
+
`wcagContrast`, `rtl`, `touchTarget` and `keyboardNavigation` — authorable but
|
|
899
|
+
never enforced, so authoring them was already a silent no-op. `@object-ui/types`
|
|
900
|
+
re-exported those sub-schemas _by reference_ (issue #2231), so they could not
|
|
901
|
+
survive the prune without becoming hand-written mirrors — exactly the second
|
|
902
|
+
de-facto contract AGENTS.md #0.1 forbids. Removed from the public surface:
|
|
903
|
+
|
|
904
|
+
- Types: `Spacing`, `Breakpoints`, `DensityMode`, `WcagContrastLevel`,
|
|
905
|
+
`ThemeLogo`, and the deprecated `SpacingScale` alias
|
|
906
|
+
- Schemas: `SpacingSchema`, `SpacingScaleSchema`, `BreakpointsSchema`,
|
|
907
|
+
`ThemeLogoSchema`, and the `SpacingSchemaType` / `BreakpointsSchemaType` helpers
|
|
908
|
+
- `Theme.spacing`, `Theme.breakpoints` and `Theme.logo`
|
|
909
|
+
|
|
910
|
+
`mergeThemes` no longer merges the three dropped keys. `generateThemeVars` is
|
|
911
|
+
unaffected — it never emitted them, which is why the liveness audit called them
|
|
912
|
+
dead. The one real consumer was `ThemeProvider`, which set the favicon from
|
|
913
|
+
`theme.logo.favicon`; that path is gone, because v17 strips the key at parse and
|
|
914
|
+
it could never arrive again. The live favicon is unaffected: it comes from
|
|
915
|
+
operator branding (`getFaviconUrl()`), applied in the console's `index.html`,
|
|
916
|
+
`main.tsx`, and on route change.
|
|
917
|
+
|
|
918
|
+
Nothing else read the pruned types. In particular the list-density feature is
|
|
919
|
+
untouched — `useDensityMode` and `rowHeightToDensityMode` use `@object-ui/core`'s
|
|
920
|
+
own local `DensityMode`, which never came from the spec.
|
|
921
|
+
|
|
922
|
+
**The spec adopted objectui's ListColumn extensions (objectui#2231), so the
|
|
923
|
+
extension collapsed.** `ListColumnSchema` used to `.extend()` the spec with two
|
|
924
|
+
fields, each carrying a note to promote it upstream rather than grow the
|
|
925
|
+
extension; v17 did exactly that. `summary` is now the spec's
|
|
926
|
+
`union([ColumnSummarySchema, ColumnSummaryConfigSchema])` — the same enum ∪
|
|
927
|
+
`{ type, field }` form `useColumnSummary` reads — and `prefix` is the spec's
|
|
928
|
+
`ColumnPrefixSchema`. `ListColumnSchema` is now a plain by-reference re-export.
|
|
929
|
+
One behavior change rides along: `prefix.type` defaults to `'text'` on parse
|
|
930
|
+
instead of staying `undefined`, so the cell renderer always gets a value.
|
|
931
|
+
|
|
932
|
+
**Node 22 is now the floor.** Every `@objectstack` package declares
|
|
933
|
+
`engines.node: ">=22.0.0"` (objectstack#3825; Node 20 reached EOL 2026-04-30).
|
|
934
|
+
This repo claimed `>=20` and ran CI on Node 20.x, so it promised — and validated
|
|
935
|
+
— a runtime its own core dependency does not support. `engines.node` is now
|
|
936
|
+
`>=22`, CI runs Node 22.x, and the CI/deployment docs say so.
|
|
937
|
+
|
|
938
|
+
The major stays 17: per AGENTS.md the major tracks `@objectstack`'s major, which
|
|
939
|
+
is also 17, and that convention deliberately outranks semver purity — so the
|
|
940
|
+
removals above ship as a minor rather than desyncing the two.
|
|
941
|
+
|
|
942
|
+
- 390c071: feat(record): declare inputs for the seven configurable record:\* blocks, and curate six
|
|
943
|
+
|
|
944
|
+
Seven `record:*` blocks shipped with renderers that read props but declared no
|
|
945
|
+
`inputs`. That combination is the worst of both: the renderer honours
|
|
946
|
+
`limit`, `severity`, `location` …, while every authoring surface — the designer
|
|
947
|
+
panel, the AI vocabulary, the generated manifest — reports the block takes no
|
|
948
|
+
configuration. objectui#3013 recorded them as deliberately uncurated for
|
|
949
|
+
exactly that reason.
|
|
950
|
+
|
|
951
|
+
The declarations mirror what each renderer actually reads:
|
|
952
|
+
|
|
953
|
+
| block | inputs |
|
|
954
|
+
| -------------------------------------- | ---------------------------------------------------------------------------- |
|
|
955
|
+
| `record:activity` | 11 — from `RecordActivityComponentProps` |
|
|
956
|
+
| `record:chatter` / `record:discussion` | 5 — from `RecordChatterComponentProps` |
|
|
957
|
+
| `record:alert` | 8 — severity, title, body, visible, icon, action, dismissible, dismissKey |
|
|
958
|
+
| `record:quick_actions` | 7 — actionNames, requiredPermissions, location, align, inline, variant, size |
|
|
959
|
+
| `record:history` | 3 — limit, emptyText, unknownUserText |
|
|
960
|
+
| `record:reference_rail` | 1 — hideEmpty |
|
|
961
|
+
|
|
962
|
+
`inputs` describe what an AUTHOR writes, which is a subset of what the renderer
|
|
963
|
+
reads. `entries`, `loading` and resolved `actions` are injected by the host
|
|
964
|
+
shell off RecordContext; declaring them would invite a model to hand-write the
|
|
965
|
+
data the page is supposed to fetch. `aria` is omitted for the reason it is
|
|
966
|
+
omitted on `record:details` — an accessibility escape hatch, not a layout
|
|
967
|
+
choice. `location` takes its enum from the spec's `ACTION_LOCATIONS` rather
|
|
968
|
+
than restating it, per objectui#3019.
|
|
969
|
+
|
|
970
|
+
Six of the seven are now in `PUBLIC_BLOCKS`: configurable and absent from the
|
|
971
|
+
contract is the state objectui#3006 was about. The contract goes 36 → 42 tags,
|
|
972
|
+
all resolving.
|
|
973
|
+
|
|
974
|
+
`record:chatter` stays out — it is the same renderer as `record:discussion`
|
|
975
|
+
under a Salesforce-familiar name, kept for schemas already in the wild. Two
|
|
976
|
+
spellings of one block is ambiguity an authoring model cannot resolve, so the
|
|
977
|
+
vocabulary carries the spec's name. A test compares the two input lists, so the
|
|
978
|
+
day they diverge the exclusion stops being justified and fails.
|
|
979
|
+
|
|
980
|
+
A companion assertion requires every curated `record:*` tag to declare inputs.
|
|
981
|
+
A curated tag with none reads as "takes no configuration" when the renderer in
|
|
982
|
+
fact reads props — the same gap objectui#3006 opened, pointed the other way.
|
|
983
|
+
|
|
984
|
+
- 912496d: feat(types,core): the `*Validation` rule types derive from spec 17, and the engine agrees with the server — objectstack#4115
|
|
985
|
+
|
|
986
|
+
The five spec-named rule variants in `data-protocol.ts` were hand-written
|
|
987
|
+
interfaces, each labelled `(ObjectStack Spec v2.0.1)` while the installed spec
|
|
988
|
+
was `17.0.0-rc.0`. Nothing bound them to the spec, so fifteen majors of drift
|
|
989
|
+
accumulated with `tsc` silent throughout and the comment still vouching for it.
|
|
990
|
+
They are now `z.input` derivations of `ScriptValidationSchema` /
|
|
991
|
+
`StateMachineValidationSchema` / `CrossFieldValidationSchema` /
|
|
992
|
+
`ConditionalValidationSchema` / `FormatValidationSchema`, and canonicity is
|
|
993
|
+
carried by that binding plus a parity gate rather than by a comment (#3017).
|
|
994
|
+
|
|
995
|
+
`z.input`, not `z.infer`, because objectui consumes **authored** metadata as it
|
|
996
|
+
arrives over `/meta` — before the spec applies its defaults and canonicalizes
|
|
997
|
+
expressions. That is the shape actually in the JSON.
|
|
998
|
+
|
|
999
|
+
**Breaking, in the shape of the rule types** (minor per this repo's version
|
|
1000
|
+
policy — see AGENTS.md §9):
|
|
1001
|
+
|
|
1002
|
+
| | was | is |
|
|
1003
|
+
| --------------------------------- | ------------------------------------------- | ------------------------------------------------------- |
|
|
1004
|
+
| `ConditionalValidation` | `condition` + `rules[]` | `when` + `then` / `otherwise` |
|
|
1005
|
+
| `FormatValidation` | `pattern` + `flags`, 8 named formats | `regex`, the 4 formats the server implements |
|
|
1006
|
+
| `Script`/`CrossField` `condition` | `string` | `string \| { dialect, source }` |
|
|
1007
|
+
| `StateMachineValidation` | — | gains `initialStates` (objectstack#3165) |
|
|
1008
|
+
| `BaseValidation` | no `priority`, `events` included `'delete'` | gains `priority`; `'delete'` retired (objectstack#3184) |
|
|
1009
|
+
|
|
1010
|
+
`UniquenessValidation` / `AsyncValidation` / `RangeValidation` are now
|
|
1011
|
+
`@deprecated`. They have no spec counterpart — the spec removed the first two
|
|
1012
|
+
deliberately (uniqueness → a unique index, since SELECT-then-INSERT is racy;
|
|
1013
|
+
async → the form layer) — and the spec's `ValidationRuleSchema` rejects all
|
|
1014
|
+
three, so no rule in those shapes can ride in `ObjectSchema.validations`.
|
|
1015
|
+
|
|
1016
|
+
**`ObjectValidationEngine` now agrees with `objectql`'s rule-validator.** It is a
|
|
1017
|
+
client PRE-CHECK of rules the server enforces, so every disagreement cost the
|
|
1018
|
+
user something real. Fixed:
|
|
1019
|
+
|
|
1020
|
+
- **Polarity was inverted.** The server violates a rule when the predicate is
|
|
1021
|
+
TRUE; the engine violated it when the predicate was FALSE. Every
|
|
1022
|
+
spec-authored `script` / `cross_field` rule produced the opposite verdict.
|
|
1023
|
+
- **Envelope conditions were a silent no-op.** `{ dialect, source }` reached
|
|
1024
|
+
`expression.trim()`, threw, was caught, and read as "passes".
|
|
1025
|
+
- **`conditional` was a silent no-op**, reading `rule.condition` / `rule.rules`
|
|
1026
|
+
where the spec says `when` / `then`; `otherwise` was never evaluated at all.
|
|
1027
|
+
- **`format` produced FALSE REJECTIONS** — it read `rule.pattern`, and
|
|
1028
|
+
`undefined.test(...)` threw into a catch that reported a violation, blocking
|
|
1029
|
+
writes the server accepts.
|
|
1030
|
+
- **An absent `active` disabled the rule** and an absent `events` threw; both
|
|
1031
|
+
arrive absent from `/meta` because the spec defaults them at parse time.
|
|
1032
|
+
- `priority` now orders execution; `initialStates` is enforced on insert;
|
|
1033
|
+
`format`/`state_machine` only fire when the write touches the field; a broken
|
|
1034
|
+
predicate or an uncompilable `regex` fails OPEN with a warning; and a rule type
|
|
1035
|
+
the engine cannot evaluate (the spec's `json_schema`) warns instead of
|
|
1036
|
+
reporting the record as valid.
|
|
1037
|
+
|
|
1038
|
+
The default `SimpleExpressionEvaluator` is not CEL and never was; it now binds
|
|
1039
|
+
both the spec's `record.x` scope and objectui's historical bare `x`, and
|
|
1040
|
+
documents that richer predicates need a CEL-backed evaluator. `validateRecord`'s
|
|
1041
|
+
`event` parameter no longer accepts `'delete'`.
|
|
1042
|
+
|
|
1043
|
+
Gates: `packages/types/src/__tests__/validation-rule-spec-parity.test.ts` (key
|
|
1044
|
+
sets, wire shapes, the pinned `then`/`otherwise` divergence with an inverted pin
|
|
1045
|
+
that fails when objectstack#4171 is fixed upstream) and the rewritten engine
|
|
1046
|
+
suite. objectstack#4115's ledger drops 120 → 115.
|
|
1047
|
+
|
|
1048
|
+
### Patch Changes
|
|
1049
|
+
|
|
1050
|
+
- b41f401: **Authoring types are input types (framework#4074 steps 2–3): `ActionParam` takes the spec's declaration forms, `ListViewSchema` stops promising parse-output defaults, and `FormField.dependsOn` matches its runtime reader.**
|
|
1051
|
+
|
|
1052
|
+
Three public types said something different from what the platform accepts. All
|
|
1053
|
+
three divergences were found by making `packages/types`' tests compile (#3009)
|
|
1054
|
+
and then resolving the declared `p1-spec-alignment.test.ts` debt site-by-site
|
|
1055
|
+
instead of papering over it.
|
|
1056
|
+
|
|
1057
|
+
**`ActionParam` is now the authoring shape, aligned with the spec's input.**
|
|
1058
|
+
`name` / `label` / `type` become optional and `field` / `objectOverride` appear:
|
|
1059
|
+
the spec's primary way to declare a param — a bare field reference that inherits
|
|
1060
|
+
label/type/validation/options from an object field — was unrepresentable while
|
|
1061
|
+
all three were required. The _resolved_ shape the dialog consumes (after
|
|
1062
|
+
app-shell's `resolveActionParams()` inlines the reference) remains
|
|
1063
|
+
`@object-ui/core`'s `ActionParamDef`, with all three required. Authoring and
|
|
1064
|
+
resolved are different types on purpose. `label` and option labels take the
|
|
1065
|
+
spec's `I18nLabel` by import — which the new compile-time guard promptly
|
|
1066
|
+
revealed to be aliased to plain `string` in the current spec (the per-locale
|
|
1067
|
+
record is the separate `I18nObject`), so this is not a behavioural widening
|
|
1068
|
+
today; importing the alias means objectui tracks any future widening
|
|
1069
|
+
automatically.
|
|
1070
|
+
|
|
1071
|
+
**Breaking:** code destructuring `param.name` / `param.label` / `param.type` as
|
|
1072
|
+
guaranteed must now handle the field-backed form (or consume the resolved
|
|
1073
|
+
`ActionParamDef` instead, which is what dialog-side code should be doing).
|
|
1074
|
+
|
|
1075
|
+
**`ListViewInferred` is `z.input`, not `z.infer`.** The spec sub-schemas that
|
|
1076
|
+
flow into the list-view surface (`userActions`, `tabs` → `ViewTab`, `sharing`)
|
|
1077
|
+
carry `.default()`s, so the inferred output type made fields like
|
|
1078
|
+
`userActions.refresh` or a tab's `pinned`/`visible` _required_ — but nothing on
|
|
1079
|
+
the render path ever runs `.parse()`: `normalizeListViewSchema` deliberately
|
|
1080
|
+
applies no defaults ("an absent flag stays absent", its own suite). The output
|
|
1081
|
+
type therefore rejected valid authored metadata (`userActions: { sort: true }`)
|
|
1082
|
+
while promising renderers defaults that never arrive. Typing the surface as
|
|
1083
|
+
input matches both the author and the runtime object. Code that _trusted_ those
|
|
1084
|
+
phantom defaults now gets an optionality error — which is a latent bug surfacing,
|
|
1085
|
+
not a regression: the value really could be absent.
|
|
1086
|
+
|
|
1087
|
+
**`FormField.dependsOn` is `DependsOnInput`.** The runtime reader
|
|
1088
|
+
(`resolveCascadingOptions`) has always accepted a bare name, a list of names, or
|
|
1089
|
+
lookup-parameter entries `{ field, param }` — its parameter type says so. The
|
|
1090
|
+
public property said `string`, so array-authored metadata type-errored while
|
|
1091
|
+
working, and the form renderer read the key through `(f as any).dependsOn` to
|
|
1092
|
+
get past its own type. The shape now lives in `@object-ui/types` (single source
|
|
1093
|
+
of truth next to `FormField`), `@object-ui/core` imports and re-exports it, and
|
|
1094
|
+
the two `as any` reads in the components form renderer are typed.
|
|
1095
|
+
|
|
1096
|
+
**The `p1-spec-alignment.test.ts` exclusion is gone.** Its 14 errors resolved:
|
|
1097
|
+
the two "sharing in ObjectUI format" tests and the legacy-ARIA-spelling fixture
|
|
1098
|
+
are deleted/rewritten — those dialects are _normalizer input_, folded by
|
|
1099
|
+
`normalizeListViewSchema` and asserted branch-by-branch in core's
|
|
1100
|
+
`normalize-list-view.test.ts`, the seam where the fold actually runs; asserting
|
|
1101
|
+
them on the canonical type only ever "passed" because nothing compiled the file.
|
|
1102
|
+
One fixture claimed a shape no surface ever admitted (an ObjectQL triplet as a
|
|
1103
|
+
spec `ViewTab.filter`) and was corrected to the rule-object form. Every test
|
|
1104
|
+
file in `@object-ui/types` is now compiled, with no exclusions.
|
|
1105
|
+
|
|
1106
|
+
Discrimination-checked: reverting `ListViewInferred` to `z.infer`, `dependsOn`
|
|
1107
|
+
to `string`, or `ActionParam.name` to required each produces the expected
|
|
1108
|
+
compile error in the now-compiled test files (`TS2739` / `TS2322` / `TS2741`);
|
|
1109
|
+
restored, all projects are clean.
|
|
1110
|
+
|
|
1111
|
+
- 95b7214: fix(list,grid,detail,tree,core): every column resolver reads one key (#3104 PR2)
|
|
1112
|
+
|
|
1113
|
+
PR1 (#3119) put a canonicalizing fold at ListView's ingestion boundary. This
|
|
1114
|
+
converges the 22 read sites themselves onto `columnIdentity()` from
|
|
1115
|
+
`@object-ui/core`, so a surface that is NOT downstream of that fold resolves
|
|
1116
|
+
the same identity anyway.
|
|
1117
|
+
|
|
1118
|
+
That distinction is the user-visible part. A standalone `object-grid` node —
|
|
1119
|
+
authored directly on a page, with no `list-view` above it — never passed
|
|
1120
|
+
through `normalizeListViewSchema`. Its `getSelectFields` read `c.field` alone
|
|
1121
|
+
while the `ensureId` probe one line above read `f?.name || f?.field`, so a
|
|
1122
|
+
legacy `{ name: 'account' }` column reached `$select` as a literal `undefined`
|
|
1123
|
+
hole: the server never returned the field and every cell in that column came
|
|
1124
|
+
back empty. Same for `ObjectTree`, `RelatedList` and the `record:details` /
|
|
1125
|
+
`record:related_list` renderers.
|
|
1126
|
+
|
|
1127
|
+
Converged:
|
|
1128
|
+
|
|
1129
|
+
| Surface | Was | Now |
|
|
1130
|
+
| ---------------------------------------- | ---------------------------------------------- | ----------------------------------- |
|
|
1131
|
+
| `ListView` ×9 + its 2 request builders | `name \|\| fieldName \|\| field` vs `f?.field` | `columnIdentity()` |
|
|
1132
|
+
| `RelatedList` ×8 | `accessorKey \|\| field \|\| name` | `accessorKey \|\| columnIdentity()` |
|
|
1133
|
+
| `ObjectGrid` | name-first probe vs `c.field` projection | `columnIdentity()` |
|
|
1134
|
+
| `ObjectTree` | `name \|\| fieldName \|\| field \|\| key` | `columnIdentity() \|\| key` |
|
|
1135
|
+
| `buildExpandFields` | `field ?? name ?? fieldName` | `columnIdentity()` |
|
|
1136
|
+
| `record-details` / `record-related-list` | `field \|\| name (\|\| key)` | `columnIdentity() (\|\| key)` |
|
|
1137
|
+
|
|
1138
|
+
`accessorKey` keeps its precedence in `RelatedList` — it is TanStack Table's
|
|
1139
|
+
column key, not ObjectStack metadata identity, and only the `field || name`
|
|
1140
|
+
tail was converged. `key` stays a tail fallback in `ObjectTree` and
|
|
1141
|
+
`record-related-list` for the same reason: it is a generic entry key.
|
|
1142
|
+
|
|
1143
|
+
Two incidental fixes that TypeScript surfaced once the resolver stopped
|
|
1144
|
+
returning `any`: ListView's filter-field options and its hide-fields popover
|
|
1145
|
+
both built entries keyed `undefined` for a column with no resolvable identity.
|
|
1146
|
+
Those entries could never match a column; they are now dropped.
|
|
1147
|
+
|
|
1148
|
+
**Inventory re-triage.** PR1 recorded 24 family members. Two were mis-classified
|
|
1149
|
+
and are reclassified here rather than converged — reading what they actually
|
|
1150
|
+
feed shows they are not column reads at all:
|
|
1151
|
+
|
|
1152
|
+
- `ViewPreview.tsx` adapts a ViewItem **form** section to what `object-form`
|
|
1153
|
+
selects by (`field` → `name`) — the #3090 two-layer join.
|
|
1154
|
+
- `SchemaForm.tsx` renders an arbitrary metadata **array** into a popover
|
|
1155
|
+
summary and guesses at a display key; the entries are validations, actions,
|
|
1156
|
+
or whatever the JSON schema declares.
|
|
1157
|
+
|
|
1158
|
+
So the family was 22, and it is now **0**. The ratchet asserts that, asserts
|
|
1159
|
+
each converged surface actually routes through the shared reader (a surface
|
|
1160
|
+
that dropped identity resolution instead of converging it goes red), and pins
|
|
1161
|
+
`accessorKey`'s precedence in `RelatedList`.
|
|
1162
|
+
|
|
1163
|
+
- 7d9734d: feat(core): say which column identity key won, out loud (#3104 PR3)
|
|
1164
|
+
|
|
1165
|
+
Closes the battle opened in #3104. PR1 (#3119) put the canonicalizing fold at
|
|
1166
|
+
ingestion; PR2 (#3122) converged all 22 read sites onto `columnIdentity()`.
|
|
1167
|
+
This is the audible half.
|
|
1168
|
+
|
|
1169
|
+
A column carrying two identity keys that **disagree** — `{ field: 'account',
|
|
1170
|
+
name: 'account_name' }` — now logs a one-time dev-mode warning naming which key
|
|
1171
|
+
won and what to change:
|
|
1172
|
+
|
|
1173
|
+
```
|
|
1174
|
+
[ObjectUI] Column carries two identities: `field: 'account'` and
|
|
1175
|
+
`name: 'account_name'`. `field` wins — it is the only key `ListColumnSchema`
|
|
1176
|
+
declares — and `name` has been rewritten to match, so the rendered column and
|
|
1177
|
+
the requested field agree. Fix the producer: drop `name` and author `field`
|
|
1178
|
+
only. (objectui#3104)
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
The fold making the two halves agree is what stops the bug, but silently
|
|
1182
|
+
rewriting `name` to match `field` also hides that the producer is emitting a
|
|
1183
|
+
contradiction. The renderer recovering is not the same as the metadata being
|
|
1184
|
+
right, so the recovery says so.
|
|
1185
|
+
|
|
1186
|
+
Deliberately narrow:
|
|
1187
|
+
|
|
1188
|
+
- **Only contradictions.** A legacy-only column (`{ name: 'stage' }`) is legacy,
|
|
1189
|
+
not conflicting — it is stamped without noise.
|
|
1190
|
+
- **Warn once per (identity, conflicting spelling).** Columns are re-normalized
|
|
1191
|
+
on every render; a warning that floods the console is a warning that gets
|
|
1192
|
+
muted. Keyed by the pair rather than the identity alone, so a column carrying
|
|
1193
|
+
two different stale spellings reports both — the author needs to fix every
|
|
1194
|
+
producer, not just the first one seen.
|
|
1195
|
+
- **Silent under `NODE_ENV=production`**, and the fold still runs there.
|
|
1196
|
+
|
|
1197
|
+
`resetColumnIdentityWarnings()` is exported for tests.
|
|
1198
|
+
|
|
1199
|
+
**No lint rule, and that is a measured decision.** #3104 asked for
|
|
1200
|
+
`no-restricted-syntax` on `.field ?? .name` to be evaluated on its
|
|
1201
|
+
false-positive rate first. With the family at zero, all 12 remaining scanner
|
|
1202
|
+
hits are legitimate — a syntactic rule cannot tell a two-layer join from a dual
|
|
1203
|
+
read, because the distinction is what the keys mean in that layer, not how the
|
|
1204
|
+
expression is spelled. Adopting it would mean 12 inline disables on correct
|
|
1205
|
+
code, which trains the next author to reach for the disable. The ratchet carries
|
|
1206
|
+
a `verdict` and a `why` per site instead, so a new hit gets triaged rather than
|
|
1207
|
+
silenced. The evaluation is written into the ratchet's header.
|
|
1208
|
+
|
|
1209
|
+
**Ledger item resolved with no change needed.** #3104 flagged `ListColumn` for
|
|
1210
|
+
disposition under objectstack#4115 (spec-named symbols must be imports, not
|
|
1211
|
+
declarations). `ListColumnSchema` is already a by-reference re-export of
|
|
1212
|
+
`@objectstack/spec/ui`, and `spec-subschema-parity.test.ts` already pins it by
|
|
1213
|
+
reference identity — the only check that distinguishes a re-export from a
|
|
1214
|
+
faithful fork. Already compliant; nothing to do.
|
|
1215
|
+
|
|
1216
|
+
- aebfa4f: chore(core): deprecate `ObjectValidationEngine` — rule enforcement stays single-implementation on the server (#3110)
|
|
1217
|
+
|
|
1218
|
+
`ObjectValidationEngine` / `defaultObjectValidationEngine` / `validateRecord` are
|
|
1219
|
+
now `@deprecated`. **Nothing is removed and no behaviour changes** — existing
|
|
1220
|
+
callers keep working exactly as they did after #3103.
|
|
1221
|
+
|
|
1222
|
+
**Why.** Object-level validation rules are _enforcement_, and enforcement is
|
|
1223
|
+
single-implementation on the server (`objectql`'s rule-validator). objectui
|
|
1224
|
+
already draws that line for the predicates it _does_ evaluate client-side:
|
|
1225
|
+
`evaluator/fieldRules.ts` handles the presentation predicates (`visibleWhen` /
|
|
1226
|
+
`readonlyWhen` / `requiredWhen`) by delegating to the canonical
|
|
1227
|
+
`ExpressionEngine`, "rather than re-implementing a parallel evaluator"
|
|
1228
|
+
(ADR-0036). This engine was that parallel evaluator, on the enforcement side.
|
|
1229
|
+
|
|
1230
|
+
#3103 converged its semantics onto the server rule-for-rule, with eight
|
|
1231
|
+
mutation-tested gates — and still left a known divergence: the server carries
|
|
1232
|
+
ADR-0113's legacy-violation exemption (reject only when the merged state violates
|
|
1233
|
+
_and_ this write makes it worse), which this engine does not implement. Editing
|
|
1234
|
+
an unrelated field on a legacy row would be blocked here and accepted there. One
|
|
1235
|
+
careful pass still left a gap, which is the argument: mirroring cross-repo
|
|
1236
|
+
behaviour is structurally unreliable, not unreliable-this-time.
|
|
1237
|
+
|
|
1238
|
+
**What to use instead.** Let the write fail and render the server's rejection —
|
|
1239
|
+
it is already structured (`field` / `code` / `message`, plus a label since
|
|
1240
|
+
objectstack#3957). For pre-submit feedback, the answer is a validate-only
|
|
1241
|
+
(dry-run) write on the server: identical UX, zero parity risk, and it covers the
|
|
1242
|
+
two rule kinds a client can never check — `unique` (needs the database) and
|
|
1243
|
+
`json_schema` (ajv lives server-side).
|
|
1244
|
+
|
|
1245
|
+
**The decision is a mechanism, not a comment.** What #3103 removed was a doc
|
|
1246
|
+
comment claiming spec canonicity that had been false for fifteen majors;
|
|
1247
|
+
shipping its successor as another comment would repeat the mistake one level up.
|
|
1248
|
+
`validation-engine-stays-unwired.test.ts` scans `packages/*/src` and fails if a
|
|
1249
|
+
production module starts referencing the engine, naming the file and the issue to
|
|
1250
|
+
reverse first. Barrels still re-export it — publishing a deprecated API is not
|
|
1251
|
+
wiring — and host applications are free to keep importing it.
|
|
1252
|
+
|
|
1253
|
+
The five spec-derived rule TYPES in `@object-ui/types` are unaffected: they are
|
|
1254
|
+
the anchor for objectstack#4115's ledger and are independent of whether objectui
|
|
1255
|
+
ships an engine.
|
|
1256
|
+
|
|
1257
|
+
- c4d7b20: fix(view,list,core): a view's filter no longer disappears, or arrives as a predicate on columns that don't exist
|
|
1258
|
+
|
|
1259
|
+
Sweeping the other `$filter` producers after #3078 turned up two live defects in
|
|
1260
|
+
`ObjectView`, which fetches its own data for calendar / kanban / gallery /
|
|
1261
|
+
timeline (grid delegates to `ObjectGrid`).
|
|
1262
|
+
|
|
1263
|
+
**1. An object filter was dropped, and only for non-grid views.**
|
|
1264
|
+
`table.defaultFilters` is declared `Record<string, any>`, and the merge tested
|
|
1265
|
+
`baseFilter.length > 0` — `undefined > 0` for an object. So the filter vanished
|
|
1266
|
+
and the view returned **every record**. `ObjectGrid` assigns the same value
|
|
1267
|
+
straight to `params.$filter`, so one view definition filtered correctly as a
|
|
1268
|
+
grid and returned everything as a calendar.
|
|
1269
|
+
|
|
1270
|
+
**2. Rule objects were spread into the `and`, not wrapped.**
|
|
1271
|
+
`['and', ...baseFilter, ...userFilter]` is only correct when the source is an
|
|
1272
|
+
array of AST nodes. `activeView.filter` is a spec `ViewFilterRule[]`, so
|
|
1273
|
+
spreading put bare rule objects where the AST expects nodes:
|
|
1274
|
+
|
|
1275
|
+
```js
|
|
1276
|
+
isFilterAST([
|
|
1277
|
+
"and",
|
|
1278
|
+
{ field: "stage", operator: "eq", value: "won" },
|
|
1279
|
+
["owner", "=", "me"],
|
|
1280
|
+
]);
|
|
1281
|
+
// false → 400 since objectstack#4121
|
|
1282
|
+
parseFilterAST(same);
|
|
1283
|
+
// {$and:[{field:'stage',operator:'eq',value:'won'}, {owner:'me'}]}
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
That second line is a predicate over three columns named `field`, `operator`
|
|
1287
|
+
and `value` — which don't exist.
|
|
1288
|
+
|
|
1289
|
+
> **Correction.** The first version of this note said the spread was "reachable
|
|
1290
|
+
> whenever a view with a filter meets a user filter value". That was wrong for
|
|
1291
|
+
> `ObjectView`: the branch required a non-empty user filter, and nothing ever
|
|
1292
|
+
> wrote the state it was built from, so it could never run. The shape is
|
|
1293
|
+
> genuinely broken — a live server answers it with a 400 — and the adapter-level
|
|
1294
|
+
> defence added alongside is still warranted for any producer that emits it, but
|
|
1295
|
+
> **this particular site was dead code, not a live defect.** Defect 1 above was
|
|
1296
|
+
> live: it sat on the always-taken path. The dead machinery behind the wrong
|
|
1297
|
+
> claim is removed in a follow-up.
|
|
1298
|
+
|
|
1299
|
+
New in `@object-ui/core`: `toFilterNode` normalizes one source (rule array / AST
|
|
1300
|
+
/ MongoDB object) and `mergeFilterNodes` combines sources as siblings under one
|
|
1301
|
+
`and`. `ObjectView` and `ListView.buildEffectiveFilter` both use them, so the
|
|
1302
|
+
three filter shapes are reconciled in one place instead of by hand at each
|
|
1303
|
+
renderer.
|
|
1304
|
+
|
|
1305
|
+
`ObjectStackAdapter` also now translates a bare rule object sitting directly
|
|
1306
|
+
under a logical node — the chokepoint defence for any producer still emitting
|
|
1307
|
+
the spread shape. Only rule-_shaped_ objects are touched; a child with no
|
|
1308
|
+
`field` is a genuine MongoDB condition and passes through untouched.
|
|
1309
|
+
|
|
1310
|
+
**Correcting a comment shipped in #3078.** `buildEffectiveFilter` documented the
|
|
1311
|
+
dropped-object case as unreachable, "nothing in this repo produces one for a
|
|
1312
|
+
list view". That was wrong: `ObjectView` passes `mergedFilters` straight into
|
|
1313
|
+
that schema's `filter`, and its last fallback is `table.defaultFilters`. The
|
|
1314
|
+
case is now handled rather than explained away.
|
|
1315
|
+
|
|
1316
|
+
Verified with 19 tests across the four packages; reverting each source file
|
|
1317
|
+
fails the ones that cover it. Emitted filters are asserted against the spec's
|
|
1318
|
+
own `isFilterAST` / `parseFilterAST`, including an executable pin on what the
|
|
1319
|
+
old spread shape produced.
|
|
1320
|
+
|
|
1321
|
+
- 6f29aa5: fix(grid): a legacy string row action runs instead of green-toasting a no-op — objectui#2960
|
|
1322
|
+
|
|
1323
|
+
A list view declaring `rowActions: ['convert_lead']` rendered a menu item that
|
|
1324
|
+
performed **zero network requests** and reported success. Where the object also
|
|
1325
|
+
declared the same action with `locations: ['list_item']`, the row menu showed a
|
|
1326
|
+
working entry and a dead duplicate of it side by side.
|
|
1327
|
+
|
|
1328
|
+
**The name never became an action.** `ObjectGrid` dispatched the legacy form as
|
|
1329
|
+
`{ type: <action name>, params: { record } }` — the action _name_ landing in the
|
|
1330
|
+
runner's `type` slot, never resolved against the object's action defs. It
|
|
1331
|
+
matches no built-in type and (absent a handler registered under that exact name)
|
|
1332
|
+
no handler either, so it fell through to `ActionRunner.executeActionSchema`,
|
|
1333
|
+
which returned `{success: true, reload: true, close: true}` for a schema with
|
|
1334
|
+
nothing in it. `handlePostExecution` then fired the green "Action completed
|
|
1335
|
+
successfully" toast.
|
|
1336
|
+
|
|
1337
|
+
Two changes, either of which would have surfaced the bug:
|
|
1338
|
+
|
|
1339
|
+
**① `ObjectGrid` resolves legacy names against `objectDef.actions`.** A name
|
|
1340
|
+
that matches a declared action is promoted to that def and dispatched through
|
|
1341
|
+
the same path as a `list_item` action — so it actually runs, and it picks up the
|
|
1342
|
+
def's label, `visible`/`disabled` predicates, param dialog and capability gate,
|
|
1343
|
+
none of which the string form could carry. A name that matches an action already
|
|
1344
|
+
rendered as a def is dropped, which is what removes the dead twin. Names that
|
|
1345
|
+
resolve to nothing are still dispatched by name, since a consumer may have
|
|
1346
|
+
registered a runner handler under exactly that name.
|
|
1347
|
+
|
|
1348
|
+
**② `ActionRunner`'s empty-schema fallthrough fails loudly.** It no longer
|
|
1349
|
+
reports success for an action it never ran: a dispatch with no registered
|
|
1350
|
+
handler and no `api`/`endpoint`/`navigate`/`redirect`/`onClick` returns a
|
|
1351
|
+
failure naming the action. Schema-only shapes that _do_ declare something — a
|
|
1352
|
+
bare `redirect`, an explicit `reload`/`close` — run exactly as before.
|
|
1353
|
+
|
|
1354
|
+
- ad0183a: fix(data-objectstack,core): an object filter no longer depends on whether the query expands a lookup
|
|
1355
|
+
|
|
1356
|
+
#3072 single-sourced the ARRAY branch of the adapter's two `find()` routes. The
|
|
1357
|
+
object branch was left as it was: `convertQueryParams` converted a MongoDB-style
|
|
1358
|
+
filter to AST while `translateFilterToAST` returned it verbatim — so the same
|
|
1359
|
+
`$filter` went out in two formats, decided by whether the query happened to
|
|
1360
|
+
expand a lookup.
|
|
1361
|
+
|
|
1362
|
+
Measured across 21 operator shapes, **four diverged**. Most of the gap turned
|
|
1363
|
+
out to be harmless — `{$and: […]}` survives the plain route as a
|
|
1364
|
+
`['$and','=',[…]]` comparison that `parseFilterAST` reads back as a real `$and`,
|
|
1365
|
+
and `$exists` vs `$null` is a difference the server treats identically. Two were
|
|
1366
|
+
not harmless:
|
|
1367
|
+
|
|
1368
|
+
- **The unknown-operator guard only ran on one route.** `convertFiltersToAST`
|
|
1369
|
+
throws on an unrecognised operator, with a comment saying it does so "to avoid
|
|
1370
|
+
silent failure" — but the expanded route never called it, so a typo'd operator
|
|
1371
|
+
threw on a plain read and shipped silently whenever a lookup was expanded.
|
|
1372
|
+
- **`$regex` was silently rewritten to `contains`.** `$regex: 'a.c'` matches
|
|
1373
|
+
"abc"; `contains 'a.c'` matches only those three literal characters. That is a
|
|
1374
|
+
_different question_, not a weaker version of the same one, and neither result
|
|
1375
|
+
looks wrong on screen. The rewrite sat behind a `console.warn`, which is not
|
|
1376
|
+
an error channel in a deployed app — and the function's own unknown-operator
|
|
1377
|
+
message never listed `$regex` among the supported set. The spec has no
|
|
1378
|
+
`$regex` (`FILTER_OPERATORS`, `data/filter.zod.ts`), so there is nothing to
|
|
1379
|
+
translate it into: it is now refused, the same treatment the neighbouring
|
|
1380
|
+
unknown operator already got. Nothing in the repo depended on the conversion.
|
|
1381
|
+
|
|
1382
|
+
Both refusals now throw `FilterOperatorError`, carrying `code: 'INVALID_FILTER'`
|
|
1383
|
+
/ `httpStatus: 400`. The pre-existing unknown-operator throw was a bare `Error`,
|
|
1384
|
+
which `classifyLoadError` classifies as a network fault — so a malformed filter
|
|
1385
|
+
told the user to check their connection (#3066), the one thing it definitely
|
|
1386
|
+
was not.
|
|
1387
|
+
|
|
1388
|
+
- aa1240a: fix(sdui): lazily-registered public blocks reach a `kind:'react'` page's scope, and ReactRunner keeps the errors it catches
|
|
1389
|
+
|
|
1390
|
+
Two defects in the trusted `kind:'react'` page tier.
|
|
1391
|
+
|
|
1392
|
+
**objectui#2953 — the contract skipped lazy blocks.** `getPublicConfigs()`
|
|
1393
|
+
resolved every curated `PUBLIC_BLOCKS` tag through `getConfig()`, which reads
|
|
1394
|
+
loaded registrations only, so a block registered with `registerLazy()` was
|
|
1395
|
+
absent from the contract until its plugin chunk happened to be imported. In
|
|
1396
|
+
`apps/console` that silently dropped `object-kanban`, `object-calendar`,
|
|
1397
|
+
`object-gantt`, `object-timeline`, `object-map` and `markdown` from every react
|
|
1398
|
+
page's scope — writing `<ObjectKanban/>` threw `ReferenceError` even though the
|
|
1399
|
+
tag is a first-class contract member, and whether it threw depended on load
|
|
1400
|
+
order. `getPublicConfigs()` now resolves pending lazy stubs too, returning them
|
|
1401
|
+
with `lazy: true` and no `component` (new `PublicComponentConfig` type); the
|
|
1402
|
+
injected wrapper renders through `SchemaRenderer`, which triggers the loader and
|
|
1403
|
+
shows its placeholder. `getConfig()` stays loaded-only by design.
|
|
1404
|
+
|
|
1405
|
+
**objectui#2954 — ReactRunner discarded its own error state.**
|
|
1406
|
+
`getDerivedStateFromProps` re-transpiled and re-evaluated the page source on
|
|
1407
|
+
every render and unconditionally set `error: null`. React runs it before the
|
|
1408
|
+
re-render that follows `getDerivedStateFromError`, so the boundary threw away
|
|
1409
|
+
the error it had just caught, rebuilt an identical throwing element, and the
|
|
1410
|
+
throw escaped past its own `fallback` to the renderer's generic panel; `onError`
|
|
1411
|
+
was gated on state that had already been cleared and never fired for a
|
|
1412
|
+
compile-time error at all; and each compile minted a fresh page function — a new
|
|
1413
|
+
element type — that remounted the subtree and wiped the page's `useState`. The
|
|
1414
|
+
transpile+eval is now memoised on `(code, scope)`, errors persist until the
|
|
1415
|
+
inputs actually change, and `onError` reports each error exactly once.
|
|
1416
|
+
|
|
1417
|
+
- d10f526: fix(sdui): the curated contract lists `record:line_items`, the tag that actually resolves
|
|
1418
|
+
|
|
1419
|
+
`PUBLIC_BLOCKS` carried `line_items` — the bare tag. `@object-ui/plugin-form`
|
|
1420
|
+
registers the block as `record:line_items` with `skipFallback: true`, which
|
|
1421
|
+
exists precisely so the bare name is _not_ claimed, so that key never existed
|
|
1422
|
+
and the curated entry could never resolve. Its four siblings in the list are all
|
|
1423
|
+
`record:`-prefixed, and plugin-form's own comment says "Register
|
|
1424
|
+
record:line_items"; the bare spelling was a slip.
|
|
1425
|
+
|
|
1426
|
+
The effect was a block that has shipped all along — a full renderer, a label,
|
|
1427
|
+
five declared `inputs` — being absent from the public contract, from the JSX
|
|
1428
|
+
type surface, from the generated manifest, and from every `kind:'react'` page's
|
|
1429
|
+
scope. It read as an unimplemented aspirational entry, which is how it was
|
|
1430
|
+
recorded when objectui#2979 added the contract-coverage guard.
|
|
1431
|
+
|
|
1432
|
+
With the tag corrected the contract has no gaps left: all 36 curated tags
|
|
1433
|
+
resolve in the console, `record:line_items` among them with its full `inputs`.
|
|
1434
|
+
The guard's known-unimplemented list is now empty and stays asserted, so the
|
|
1435
|
+
next entry that cannot resolve surfaces instead of being explained away.
|
|
1436
|
+
|
|
1437
|
+
- 2d5d594: fix(list,detail): sorting a lookup column no longer orders by an invisible key — #3096
|
|
1438
|
+
|
|
1439
|
+
A relational column (`lookup` / `master_detail` / `user` / `tree`) never holds
|
|
1440
|
+
the string its cell shows: it holds the `$expand`-ed record, or a raw foreign-key
|
|
1441
|
+
id whose label was resolved separately. Every sort path took that raw value as
|
|
1442
|
+
its key, so the column of names came back in an order with no relation to the
|
|
1443
|
+
names — sorting looked broken, with nothing saying the key was something else.
|
|
1444
|
+
|
|
1445
|
+
The two halves are fixed differently, because they can order by different things:
|
|
1446
|
+
|
|
1447
|
+
- **Client-side sorts** (grid column headers, any `data-table`, a non-windowed
|
|
1448
|
+
related list) now key off the label the cell renders, via the new
|
|
1449
|
+
`getSortValue` / `compareSortValues` in `@object-ui/core` — which resolves an
|
|
1450
|
+
expanded record through `getRecordDisplayName` (ADR-0079), so the sort key and
|
|
1451
|
+
the lookup cell agree on which field names a record. This replaces two broken
|
|
1452
|
+
comparators: `a[col] < b[col]` is always false between two objects (the
|
|
1453
|
+
comparator collapsed to a constant and permuted the rows), and
|
|
1454
|
+
`String(a[col])` is `"[object Object]"` (every row compared equal, so the sort
|
|
1455
|
+
silently did nothing).
|
|
1456
|
+
- **Server `$orderby` sorts** cannot be fixed here — the key is the stored id by
|
|
1457
|
+
construction, and `objectstack#4256` settled that no relation join is coming.
|
|
1458
|
+
So those entry points stop offering the illusion: the ListView toolbar sort
|
|
1459
|
+
picker withholds relational fields and explains why (pointing at a formula
|
|
1460
|
+
field as the supported way to sort by a related name), and a windowed related
|
|
1461
|
+
list renders no sort button for them.
|
|
1462
|
+
|
|
1463
|
+
A relational field the view's CURRENT sort already uses stays listed, labelled
|
|
1464
|
+
`(by ID)`, so view metadata authored or saved with such a sort round-trips
|
|
1465
|
+
instead of rendering a blank row and losing the sort on the next edit.
|
|
1466
|
+
|
|
1467
|
+
- 7f23cd0: fix(form): a numeric/boolean select option survives selection with its type intact — #3090
|
|
1468
|
+
|
|
1469
|
+
`SelectOptionSchema.value` has accepted `string | number | boolean` for as
|
|
1470
|
+
long as it has existed, but the Radix controls underneath speak strings:
|
|
1471
|
+
picking `{ value: 2 }` silently submitted `"2"` — a wrong-typed write into a
|
|
1472
|
+
number field that nothing on the client ever reported. (Display half-worked:
|
|
1473
|
+
a numeric default matched its numeric item; only SELECTION morphed the type.)
|
|
1474
|
+
|
|
1475
|
+
The renderers now stringify on the way into the control and map the selection
|
|
1476
|
+
back to the AUTHORED option value on the way out (`matchOptionValue`), across
|
|
1477
|
+
the in-form select, the standalone `type: 'select'` component, and the
|
|
1478
|
+
standalone `type: 'radio-group'` component. The TS types stop lying to match:
|
|
1479
|
+
`SelectOption.value` / `RadioOption.value` and the corresponding
|
|
1480
|
+
`value`/`defaultValue`/`onChange` channels widen to what the zod schemas
|
|
1481
|
+
always accepted — a call site treating `option.value` as `string` is now a
|
|
1482
|
+
compile error pointing at a real latent crash, not a false comfort.
|
|
1483
|
+
|
|
1484
|
+
The ripple the widening named, handled at each boundary: `@object-ui/core`'s
|
|
1485
|
+
`OptionLike.value` widens (the option engines compare by identity, so values
|
|
1486
|
+
flow opaquely; the option-lint's CEL-literal domain stringifies at its
|
|
1487
|
+
boundary), and the multi-value field widgets (checkboxes / multiselect /
|
|
1488
|
+
radio) stringify at theirs — multi-value fields store string arrays.
|
|
1489
|
+
|
|
1490
|
+
Round-trip pinned by real Radix interactions in jsdom: the in-form select
|
|
1491
|
+
submits `2` (number), the standalone select hands its handler `false`
|
|
1492
|
+
(boolean).
|
|
1493
|
+
|
|
1494
|
+
- Updated dependencies [9e7349e]
|
|
1495
|
+
- Updated dependencies [8864971]
|
|
1496
|
+
- Updated dependencies [b41f401]
|
|
1497
|
+
- Updated dependencies [19e9fa0]
|
|
1498
|
+
- Updated dependencies [38ca8be]
|
|
1499
|
+
- Updated dependencies [4952edf]
|
|
1500
|
+
- Updated dependencies [7f0252e]
|
|
1501
|
+
- Updated dependencies [7639a61]
|
|
1502
|
+
- Updated dependencies [94e63ef]
|
|
1503
|
+
- Updated dependencies [c4db402]
|
|
1504
|
+
- Updated dependencies [5319bf1]
|
|
1505
|
+
- Updated dependencies [49e5671]
|
|
1506
|
+
- Updated dependencies [b5b97e2]
|
|
1507
|
+
- Updated dependencies [f59f2c1]
|
|
1508
|
+
- Updated dependencies [4874117]
|
|
1509
|
+
- Updated dependencies [ce08d55]
|
|
1510
|
+
- Updated dependencies [2374a49]
|
|
1511
|
+
- Updated dependencies [ea7f477]
|
|
1512
|
+
- Updated dependencies [7f23cd0]
|
|
1513
|
+
- Updated dependencies [24e0e0a]
|
|
1514
|
+
- Updated dependencies [3a6cf24]
|
|
1515
|
+
- Updated dependencies [aa35561]
|
|
1516
|
+
- Updated dependencies [03bd53b]
|
|
1517
|
+
- Updated dependencies [3c1f321]
|
|
1518
|
+
- Updated dependencies [a045a32]
|
|
1519
|
+
- Updated dependencies [912496d]
|
|
1520
|
+
- Updated dependencies [9867281]
|
|
1521
|
+
- @object-ui/types@17.1.0
|
|
1522
|
+
|
|
1523
|
+
## 17.0.0
|
|
1524
|
+
|
|
1525
|
+
### Minor Changes
|
|
1526
|
+
|
|
1527
|
+
- f9bbddb: feat: gate detail/form edit & delete on the server's effective operation set (#3546)
|
|
1528
|
+
|
|
1529
|
+
PR-4 (#3391) wired the **list/toolbar** surface (ObjectView Import, ListView /
|
|
1530
|
+
ObjectGrid Export) to the server-resolved effective API operation set
|
|
1531
|
+
(`/me/permissions` `apiOperations`, intersected via
|
|
1532
|
+
`resolveCrudAffordances(obj, effectiveApiOperations?)`). The **detail / form**
|
|
1533
|
+
surfaces still gated edit/delete on the bucket + `userActions` alone. This
|
|
1534
|
+
extends the same intersection to them, so the record page and its forms never
|
|
1535
|
+
offer an operation the server would 405.
|
|
1536
|
+
|
|
1537
|
+
- **core** `isObjectInlineEditable(obj, effectiveApiOperations?)` gains the same
|
|
1538
|
+
optional second argument as `resolveCrudAffordances` — inline-edit is now
|
|
1539
|
+
additionally ANDed with the server allowing `update`.
|
|
1540
|
+
- **app-shell** `RecordDetailView` threads the object's effective operations into
|
|
1541
|
+
the synthesized Edit/Delete header actions and the record-body inline-edit
|
|
1542
|
+
gate (`canEdit`); `RelatedRecordActionsBridge` intersects each **child**
|
|
1543
|
+
object's Create/Edit/Delete handlers with that child's own effective set.
|
|
1544
|
+
- **plugin-detail** `record:details` ANDs its inline-edit affordance with the
|
|
1545
|
+
object's effective `update`.
|
|
1546
|
+
- **plugin-form** `ObjectForm`'s blanket managed-object field lock also engages
|
|
1547
|
+
when the server denies `update` (edit mode) / `create` (create mode).
|
|
1548
|
+
|
|
1549
|
+
Backward-compatible: a missing effective set (unrestricted object, older
|
|
1550
|
+
backend, or no `PermissionProvider`) leaves the resolved affordance untouched —
|
|
1551
|
+
the bucket/`userActions` decision wins, exactly as today. Layers on top of the
|
|
1552
|
+
existing per-object `check('edit')` / `check('delete')` permission gates
|
|
1553
|
+
(intersection, never union).
|
|
1554
|
+
|
|
1555
|
+
- 2735de6: feat: render the server's effective API operation set (#3391 PR-4)
|
|
1556
|
+
|
|
1557
|
+
The frontend now consumes the per-object **effective API operation set** the
|
|
1558
|
+
server resolves (from `/me/permissions` `apiOperations`, framework #3391) —
|
|
1559
|
+
never the raw `apiMethods` — so Import/Export/New/Edit/Delete buttons match what
|
|
1560
|
+
the server will actually admit, and a 405 import refusal shows a dedicated
|
|
1561
|
+
message instead of silently falling back.
|
|
1562
|
+
|
|
1563
|
+
- **core** `resolveCrudAffordances(obj, effectiveApiOperations?)` — new optional
|
|
1564
|
+
second argument intersects each affordance bit with its API operation
|
|
1565
|
+
(create/import→create/import, edit→update, delete→delete, exportCsv→export).
|
|
1566
|
+
Omitting it (old backend / no effective set) leaves affordances unchanged.
|
|
1567
|
+
- **permissions** — `/me/permissions` response carries per-object
|
|
1568
|
+
`apiOperations`; `PermissionContextValue.getObjectApiOperations(object)`
|
|
1569
|
+
exposes it (undefined when absent → callers keep current behavior); `check()`
|
|
1570
|
+
maps `import→allowCreate`, `export→allowRead`.
|
|
1571
|
+
- **app-shell** `ObjectView` intersects its toolbar affordances with the object's
|
|
1572
|
+
effective operations (Import); the platform-admin identity-import bypass is
|
|
1573
|
+
unaffected.
|
|
1574
|
+
- **plugin-list** `ListView` / **plugin-grid** `ObjectGrid` gate the Export
|
|
1575
|
+
button (and export handler) on effective `export`; `plugin-grid` gains the
|
|
1576
|
+
`@object-ui/permissions` workspace dependency.
|
|
1577
|
+
- **plugin-grid** `ImportWizard` — a 405 / `OBJECT_API_METHOD_NOT_ALLOWED`
|
|
1578
|
+
import refusal is detected by a new `isImportNotAllowed` predicate at every
|
|
1579
|
+
catch site (async, sync, dry-run) and STOPS with a dedicated
|
|
1580
|
+
`grid.import.notAllowed` message (10 locales + fallback dict) — it never falls
|
|
1581
|
+
back to the sync/legacy path (which 405s too), distinct from the 404
|
|
1582
|
+
route-absent fallback.
|
|
1583
|
+
|
|
1584
|
+
Backward-compatible: a missing effective set (unrestricted object, older
|
|
1585
|
+
backend, or no permission provider) preserves the current default-allow
|
|
1586
|
+
behavior everywhere.
|
|
1587
|
+
|
|
1588
|
+
- cd09a7b: refactor(views): ListView reads the spec-canonical `columns`, with legacy `fields` folded in one normalizer (#2890 scope A step 1)
|
|
1589
|
+
|
|
1590
|
+
`ListViewSchema` has been derived from `@objectstack/spec/ui` since #2231, but
|
|
1591
|
+
the renderer still spoke objectui's own vocabulary for the same concepts. First
|
|
1592
|
+
rename closed: **`fields` → `columns`**.
|
|
1593
|
+
|
|
1594
|
+
Legacy acceptance does not disappear — stored view metadata in user databases
|
|
1595
|
+
carries `fields` — but it now lives in exactly one place instead of being
|
|
1596
|
+
re-implemented per read-site:
|
|
1597
|
+
|
|
1598
|
+
- **New `normalizeListViewSchema` (`@object-ui/core`)** folds `fields` into
|
|
1599
|
+
`columns` (canonical wins when both are present) and drops the legacy key, so
|
|
1600
|
+
a read-site that was missed fails loudly instead of quietly taking the legacy
|
|
1601
|
+
path. It also absorbs the `viewType` renderability default ListView applied
|
|
1602
|
+
inline. Non-mutating, idempotent, and returns its input by reference when
|
|
1603
|
+
there is nothing to fold, so ListView's downstream memos keep a stable
|
|
1604
|
+
dependency identity.
|
|
1605
|
+
- **`ListView` normalizes once at the component boundary**, before anything
|
|
1606
|
+
reads the schema. This is what guarantees the fold runs: nothing on the render
|
|
1607
|
+
path parses view metadata through zod (the zod schemas serve the CLI
|
|
1608
|
+
validator, the VS Code extension and tests), so a `z.preprocess` on
|
|
1609
|
+
`ListViewSchema` — spec-side or local — would never execute.
|
|
1610
|
+
- **Producers emit `columns`**: `ObjectView`'s `renderListView` payload,
|
|
1611
|
+
`ObjectDataPage`, `InterfaceListPage` and the `list-view` registry defaults
|
|
1612
|
+
had been _downgrading_ already-canonical `columns` config back to `fields`.
|
|
1613
|
+
|
|
1614
|
+
Two latent inconsistencies go away with it: the filter builder's
|
|
1615
|
+
objectDef-not-loaded fallback now resolves `ListColumn.field` (it read only
|
|
1616
|
+
`name`/`fieldName`, so object-form columns produced unnamed filter entries), and
|
|
1617
|
+
the column list no longer depends on which of the two keys a host happened to
|
|
1618
|
+
emit.
|
|
1619
|
+
|
|
1620
|
+
`fields` stays declared on `ListViewSchema` and in the drift guard's sanctioned
|
|
1621
|
+
set — it is still valid input, and `@objectstack/spec`'s `react-blocks.ts`
|
|
1622
|
+
sanctions it as the React-tier `<ListView fields>` prop — but it is input-only.
|
|
1623
|
+
|
|
1624
|
+
- f1abf0e: fix(views): ListView reads the spec-canonical `filter`, so a view's base filter reaches every visualization (#2890 scope A step 4)
|
|
1625
|
+
|
|
1626
|
+
Third rename in the ListView vocabulary migration: **`filters` → `filter`**. Unlike
|
|
1627
|
+
the first two this closes a live bug, because the fork was asymmetric.
|
|
1628
|
+
|
|
1629
|
+
`ListView` was the **only** surface in the repo reading `filters`. Every child
|
|
1630
|
+
view — `ObjectGrid`, `ObjectGallery`, `ObjectKanban`, `ObjectCalendar`,
|
|
1631
|
+
`ObjectGantt`, `ObjectMap`, `ObjectTree`, `ObjectChart` — reads `filter`, and
|
|
1632
|
+
`ListView` handed them `filters`. Wherever a child fetches its own rows instead
|
|
1633
|
+
of receiving `ListView`'s, the view's base filter was silently dropped:
|
|
1634
|
+
|
|
1635
|
+
- **a `chart` list view aggregated the whole object.** The chart branch built an
|
|
1636
|
+
`object-chart` node with `filters:`; `ObjectChart` reads `schema.filter` and
|
|
1637
|
+
never read `filters`, so a chart view with a base filter charted unfiltered
|
|
1638
|
+
totals.
|
|
1639
|
+
- the same applied to any of the other view components rendered standalone from
|
|
1640
|
+
a list-view-shaped config.
|
|
1641
|
+
|
|
1642
|
+
Conversely, a **spec-authored** list view — one carrying `filter`, which is what
|
|
1643
|
+
the spec says and what `runtime-metadata-persistence` and "Save as view" already
|
|
1644
|
+
persist — rendered **unfiltered** in `ListView`, because nothing read that key.
|
|
1645
|
+
|
|
1646
|
+
The fold is a key rename only. Both keys carry an ObjectQL FilterNode array
|
|
1647
|
+
everywhere in objectui; every consumer passes the value straight to `$filter`.
|
|
1648
|
+
(The spec types `filter` as `ViewFilterRule[]` — `{field, operator, value}`
|
|
1649
|
+
objects — so objectui's field is typed from the spec but used as something else.
|
|
1650
|
+
That mismatch is real and left alone here: converting formats inside a
|
|
1651
|
+
vocabulary fold would change what reaches the data source.)
|
|
1652
|
+
|
|
1653
|
+
Also collapses a duplicated computation in `app-shell`'s `ObjectView`, which
|
|
1654
|
+
computed the same effective filter **twice** — once as `filter` for the child
|
|
1655
|
+
views, once as `filters` for `ListView` — with the two copies subtly different
|
|
1656
|
+
(only one fell back to `listSchema.filter`; only the other ran token
|
|
1657
|
+
substitution over the URL filters). There is now one computation, keeping both
|
|
1658
|
+
behaviors.
|
|
1659
|
+
|
|
1660
|
+
`filters` stays declared on `ListViewSchema` and in the drift guard's sanctioned
|
|
1661
|
+
set — stored views carry it and it is still valid input — but it is input-only.
|
|
1662
|
+
|
|
1663
|
+
- f05b84e: refactor(views): ListView resolves density from the spec-canonical `rowHeight` (#2890 scope A step 2)
|
|
1664
|
+
|
|
1665
|
+
Second rename in the ListView vocabulary migration: **`densityMode` → `rowHeight`**,
|
|
1666
|
+
folded in the same `normalizeListViewSchema` that step 1 introduced.
|
|
1667
|
+
|
|
1668
|
+
Unlike `fields`/`columns` this is not a pure alias — the two vocabularies are
|
|
1669
|
+
different sizes. The spec has five row heights (`compact`/`short`/`medium`/
|
|
1670
|
+
`tall`/`extra_tall`); ListView's toolbar offers three densities
|
|
1671
|
+
(`compact`/`comfortable`/`spacious`). Both directions now live in one place as
|
|
1672
|
+
`DENSITY_MODE_TO_ROW_HEIGHT` / `ROW_HEIGHT_TO_DENSITY_MODE`, chosen so a fold
|
|
1673
|
+
followed by a read is a round trip (`spacious` → `tall` → `spacious`), with the
|
|
1674
|
+
narrowing collapse (`short` → `compact`, `extra_tall` → `spacious`) stated once
|
|
1675
|
+
instead of being re-derived per call site.
|
|
1676
|
+
|
|
1677
|
+
Two behavior fixes fall out of it:
|
|
1678
|
+
|
|
1679
|
+
- **Precedence is no longer inverted.** `ListView` read `densityMode` _first_, so
|
|
1680
|
+
a view carrying both keys rendered the legacy value — backwards from every
|
|
1681
|
+
other legacy/canonical pair in the schema. The canonical key now wins.
|
|
1682
|
+
- **The toolbar stops re-seeding the legacy key.** `ObjectView`'s
|
|
1683
|
+
`onDensityChange` persisted `densityMode` into stored view metadata on every
|
|
1684
|
+
density toggle, so the legacy vocabulary kept regrowing underneath the
|
|
1685
|
+
migration. It persists `rowHeight` now.
|
|
1686
|
+
|
|
1687
|
+
`densityMode` stays declared on `ListViewSchema` and in the drift guard's
|
|
1688
|
+
sanctioned set — stored views carry it and it is still valid input — but it is
|
|
1689
|
+
input-only.
|
|
1690
|
+
|
|
1691
|
+
### Patch Changes
|
|
1692
|
+
|
|
1693
|
+
- 7b35e4b: fix(dashboard,charts): resolve `{current_user_id}` in widget filters (framework #3574)
|
|
1694
|
+
|
|
1695
|
+
A dashboard widget filtered on `{current_user_id}` rendered `0`. The token
|
|
1696
|
+
reached SQL as a literal, matched no row, and nothing was logged on the client
|
|
1697
|
+
or the server — a silent zero that reads as "you have no work" rather than
|
|
1698
|
+
"this filter did not resolve". The same token in a list-view filter resolved
|
|
1699
|
+
correctly, so a user-scoped list and a user-scoped widget over the same data
|
|
1700
|
+
disagreed.
|
|
1701
|
+
|
|
1702
|
+
There was no shared resolver. Three ad-hoc implementations had grown up
|
|
1703
|
+
independently — `ObjectView` for list views, `ObjectDataPage` for URL filter
|
|
1704
|
+
triples, `NavigationRenderer` for hrefs — and each understood only the filter
|
|
1705
|
+
shape its own surface used. `ObjectView`'s opened with
|
|
1706
|
+
`if (!Array.isArray(filter)) return filter`, so it could not have been reused
|
|
1707
|
+
by dashboard widgets even in principle: widget filters are MongoDB-style
|
|
1708
|
+
objects. Widgets therefore got no resolution at all — `DatasetWidget` called
|
|
1709
|
+
`resolveDateMacros` and nothing else, which is why `{today}` worked in a widget
|
|
1710
|
+
and `{current_user_id}` silently did not.
|
|
1711
|
+
|
|
1712
|
+
- **`@object-ui/core`** — new `utils/filter-tokens.ts` with
|
|
1713
|
+
`resolveContextTokens` and `resolveFilterPlaceholders`. The latter expands
|
|
1714
|
+
_every_ placeholder vocabulary in one call and is what surfaces should use;
|
|
1715
|
+
resolving only some of them is the whole defect. The walk handles arrays and
|
|
1716
|
+
plain objects uniformly, so one resolver covers both platform filter shapes.
|
|
1717
|
+
- **`@object-ui/react`** — new `FilterScopeProvider` / `useFilterScope`. The
|
|
1718
|
+
renderer packages deliberately do not depend on `@object-ui/auth`, so the
|
|
1719
|
+
shell supplies the session values. This is a separate context from
|
|
1720
|
+
`PredicateScopeContext`, which is the expression evaluation scope and carries
|
|
1721
|
+
no organization.
|
|
1722
|
+
- **`@object-ui/plugin-dashboard` / `@object-ui/plugin-charts`** — all six
|
|
1723
|
+
widgets that previously resolved date macros only now resolve both
|
|
1724
|
+
vocabularies: `DatasetWidget`, `ObjectMetricWidget`, `ObjectDataTable`,
|
|
1725
|
+
`ObjectPivotTable`, and `ObjectChart` (dataset-bound and inline paths). The
|
|
1726
|
+
chart's `compareTo` comparison filter gets the session pass too — otherwise
|
|
1727
|
+
the overlay series silently ignored the owner clause the primary series
|
|
1728
|
+
honoured.
|
|
1729
|
+
- **`@object-ui/app-shell`** — `ObjectView`'s local `substituteFilterTokens`
|
|
1730
|
+
and `ObjectDataPage`'s inline `=== '{current_user_id}'` ternary now delegate
|
|
1731
|
+
to the shared resolver, so both also gain `{current_org_id}` and date macros.
|
|
1732
|
+
Two of the three ad-hoc implementations are gone rather than joined by a
|
|
1733
|
+
fourth.
|
|
1734
|
+
|
|
1735
|
+
An unresolvable token is left intact rather than dropped: leaving it yields an
|
|
1736
|
+
empty result, whereas dropping the clause would _widen_ the result set and show
|
|
1737
|
+
a signed-out viewer everyone's data. It is no longer silent — the resolver
|
|
1738
|
+
warns, naming the token, and suggests the intended spelling for known
|
|
1739
|
+
near-misses (`{current_user}`, `{user_id}`, `{organization_id}`). Authoring-time
|
|
1740
|
+
enforcement lands separately as `filter-token-unknown` in `@objectstack/lint`.
|
|
1741
|
+
|
|
1742
|
+
- e16ed2d: fix(dashboard,charts): send widget `dateGranularity`/`sortBy`/`limit` to the query, and give funnels a real stage order (framework#3588)
|
|
1743
|
+
|
|
1744
|
+
`DatasetWidget` never read `widget.options`. Four keys an author writes there
|
|
1745
|
+
change the query the server compiles, so a widget declaring
|
|
1746
|
+
`options: { dateGranularity: 'month' }` grouped by the raw timestamp and drew
|
|
1747
|
+
one bar per record, and `sortBy`/`sortOrder` produced no ordering at all.
|
|
1748
|
+
|
|
1749
|
+
- `DatasetWidget` lowers `options.dateGranularity`, `options.sortBy` +
|
|
1750
|
+
`options.sortOrder`, and `options.limit` into the `DatasetSelection` it posts.
|
|
1751
|
+
A `sortBy` naming something the widget does not project is dropped rather than
|
|
1752
|
+
sent, so a stale sort key left by an edit degrades to "unordered" instead of
|
|
1753
|
+
failing the widget against the server's stricter validation. These keys also
|
|
1754
|
+
join the refetch signature, so editing one in the designer refetches instead
|
|
1755
|
+
of re-rendering the previous grid.
|
|
1756
|
+
- Funnel stages follow a **declared order**. `AdvancedChartImpl` sorted funnel
|
|
1757
|
+
segments by value descending, unconditionally — which overrode any server
|
|
1758
|
+
ordering and rendered a sales pipeline as a tidy narrowing shape whatever the
|
|
1759
|
+
stages' real sequence, hiding the very anomaly (a bulge at Proposal) the chart
|
|
1760
|
+
exists to show. It now honours a `categoryOrder`, which `DatasetWidget`
|
|
1761
|
+
derives from the dimension field's picklist option order — the pipeline order
|
|
1762
|
+
an author already declared on the object — or from an explicit
|
|
1763
|
+
`options.stageOrder`. With no declared order the value-descending default is
|
|
1764
|
+
unchanged, and a category missing from the order is kept (after the declared
|
|
1765
|
+
ones), never dropped.
|
|
1766
|
+
- New `@object-ui/core` helpers `buildCategoryOrder` / `buildCategoryRank`,
|
|
1767
|
+
keyed by both the stored value and the display label like the existing
|
|
1768
|
+
`buildOptionColorMap`, so ordering works whether or not the server resolved
|
|
1769
|
+
the dimension's labels.
|
|
1770
|
+
|
|
1771
|
+
Requires the framework-side fix in objectstack#3588 for the selection keys to
|
|
1772
|
+
take effect server-side.
|
|
1773
|
+
|
|
1774
|
+
- 53642d4: fix(core,fields): a string `$orderby` is a clause, not a character array — and localize the sharing-rule widgets (objectstack#3821)
|
|
1775
|
+
|
|
1776
|
+
**The recipient picker listed nothing, ever.** `QueryParams['$orderby']` was
|
|
1777
|
+
typed as `Record | string[] | SortObject[]`, so `queryParamsToRecord` sent any
|
|
1778
|
+
non-array value through `Object.entries`. Handed the clause string `'name asc'`
|
|
1779
|
+
— which callers do build by hand — it walked the string index by index and
|
|
1780
|
+
emitted `$orderby=0 n,1 a,2 m,3 e,4 ,5 a,6 s,7 c`. The server sorted by columns
|
|
1781
|
+
that don't exist and every row was filtered out, so
|
|
1782
|
+
`sys_sharing_rule.recipient_id` rendered "No matches" for every recipient type
|
|
1783
|
+
and no sharing rule could be created from the Console. `ObjectGrid` builds the
|
|
1784
|
+
same shape from a schema-level `sort` in three places, so grids with a string
|
|
1785
|
+
sort silently showed an empty table.
|
|
1786
|
+
|
|
1787
|
+
A string `$orderby` is now passed through verbatim (the server's OData
|
|
1788
|
+
normalizer has always parsed `'name asc'`), and the type admits `string`.
|
|
1789
|
+
`RecipientPickerField` additionally switched to the structured
|
|
1790
|
+
`{ name: 'asc' }` form so it can't regress this way against any data source.
|
|
1791
|
+
|
|
1792
|
+
**The three sharing-rule authoring widgets never had translations.**
|
|
1793
|
+
`ObjectRefField`, `RecipientPickerField` and `FilterConditionField` hardcoded
|
|
1794
|
+
their English copy — a Chinese Console showed "Select an object", "Select a
|
|
1795
|
+
user", "Search…", "No matches", "Edit as JSON". They now go through
|
|
1796
|
+
`useFieldTranslation` like every other widget, with keys added under `fields.*`
|
|
1797
|
+
in all ten locales.
|
|
1798
|
+
|
|
1799
|
+
The recipient placeholder was the interesting one: it read
|
|
1800
|
+
`` `Select a ${recipientType.replace(/_/g,' ')}` ``, interpolating the enum
|
|
1801
|
+
value into an English sentence — a shape no locale can translate. It is now a
|
|
1802
|
+
per-type key (`fields.recipient.selectUser`, `…selectBusinessUnit`, …), so
|
|
1803
|
+
"选择业务单元" and "Select a business unit" no longer have to share a structure.
|
|
1804
|
+
|
|
1805
|
+
**Editing a rule silently dropped its recipient.** The picker resets the stored
|
|
1806
|
+
id when `recipient_type` changes, because an id valid for a user is meaningless
|
|
1807
|
+
for a team. It treated the edit form's `'' → 'user'` hydration as such a change:
|
|
1808
|
+
opening any saved rule blanked the recipient, and saving persisted the blank.
|
|
1809
|
+
Only a non-empty predecessor now counts as a type switch.
|
|
1810
|
+
|
|
1811
|
+
**Building a filter submitted the surrounding form.** None of `FilterBuilder`'s
|
|
1812
|
+
controls declared `type="button"`, and a bare `<button>` inside a `<form>`
|
|
1813
|
+
defaults to `type="submit"`. Adding, removing or clearing a condition therefore
|
|
1814
|
+
submitted the sharing-rule dialog — firing validation mid-edit, and on an
|
|
1815
|
+
already-valid form saving the record before the admin was done.
|
|
1816
|
+
|
|
1817
|
+
**A rejected write showed the user raw server diagnostics.** The form rendered
|
|
1818
|
+
`error.message` verbatim, so a sharing / RLS denial reached the dialog and the
|
|
1819
|
+
toast as `FORBIDDEN: insufficient privileges to update showcase_private_note
|
|
1820
|
+
pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and the
|
|
1821
|
+
record id to whoever hit it. Permission failures now render localized copy
|
|
1822
|
+
(`form.noPermissionToSave`, added in all ten locales), with the server text kept
|
|
1823
|
+
on the console for debugging; other failures still show the server's message,
|
|
1824
|
+
which is the useful part, and fall back to `form.submitFailed` when there is
|
|
1825
|
+
none — replacing the previously hardcoded English "An error occurred during
|
|
1826
|
+
submission".
|
|
1827
|
+
|
|
1828
|
+
**The detail header offered "Edit" on records the user may only read.** Object
|
|
1829
|
+
permissions can't express "this one record is read-only" — a read-only sharing
|
|
1830
|
+
grant sits inside an object the user may otherwise edit — so the header showed
|
|
1831
|
+
the primary Edit CTA, opened the form, and let the user retype a field before
|
|
1832
|
+
the server rejected the save. `DetailView` now gates Edit / Delete on the
|
|
1833
|
+
object-level check AND on the explain engine's record-grained verdict
|
|
1834
|
+
(`POST /api/v1/security/explain` with a `recordId`, ADR-0090 D6 / ADR-0095 C2 —
|
|
1835
|
+
the same pipeline the enforcement middleware runs, so button and server cannot
|
|
1836
|
+
disagree). Explaining oneself needs no special permission. The probe is one
|
|
1837
|
+
cached request per record, skipped entirely when the object-level check already
|
|
1838
|
+
says no, and **fails open** on every uncertainty — an unanswered hint must never
|
|
1839
|
+
be the reason a permitted user cannot act; the server stays the authority
|
|
1840
|
+
(ADR-0057 D10).
|
|
1841
|
+
|
|
1842
|
+
**A long option rendered straight past the combobox border.** `Combobox`'s
|
|
1843
|
+
trigger pinned itself to the component's `w-[200px]` default while the fields
|
|
1844
|
+
around it ran the full form column, and the selected label was a bare text child
|
|
1845
|
+
of a flex button — flex items need `truncate` AND `min-w-0` to clip, and it had
|
|
1846
|
+
neither. So "成员 (showcase_project_membership)" in the object picker overflowed
|
|
1847
|
+
the control and collided with the field beside it. The label now truncates, the
|
|
1848
|
+
trigger can shrink, the dropdown matches the trigger's width instead of a
|
|
1849
|
+
hardcoded 200px (a widened combobox used to clip its own options), and the two
|
|
1850
|
+
sharing-rule pickers ask for `w-full` so they line up with every other input.
|
|
1851
|
+
|
|
1852
|
+
Hardens `evaluatePermission` while there: a role config carrying only
|
|
1853
|
+
`fieldPermissions` (no `actions`) made `check()` throw a TypeError that
|
|
1854
|
+
propagated out of the render. A permission check must not be able to crash a
|
|
1855
|
+
view.
|
|
1856
|
+
|
|
1857
|
+
Browser-verified against the framework showcase Console in Chinese: object /
|
|
1858
|
+
criteria / recipient copy is fully localized, the recipient dropdown lists real
|
|
1859
|
+
users, business units and positions, a saved rule reopens with its recipient and
|
|
1860
|
+
criteria intact, editing the filter no longer submits, and a rule created
|
|
1861
|
+
end-to-end stores a real record id rather than free text. The criteria authored
|
|
1862
|
+
in the builder is honored by the evaluator: `{"pinned":true}` on an owner-private
|
|
1863
|
+
object granted the recipient exactly the matching records and nothing else.
|
|
1864
|
+
|
|
1865
|
+
- Updated dependencies [1767124]
|
|
1866
|
+
- Updated dependencies [8ecf5a6]
|
|
1867
|
+
- Updated dependencies [dfd3705]
|
|
1868
|
+
- Updated dependencies [6dee2cb]
|
|
1869
|
+
- Updated dependencies [c7cff19]
|
|
1870
|
+
- Updated dependencies [cd09a7b]
|
|
1871
|
+
- Updated dependencies [f1abf0e]
|
|
1872
|
+
- Updated dependencies [f05b84e]
|
|
1873
|
+
- Updated dependencies [662bdf9]
|
|
1874
|
+
- Updated dependencies [059a052]
|
|
1875
|
+
- Updated dependencies [53642d4]
|
|
1876
|
+
- Updated dependencies [8aae006]
|
|
1877
|
+
- Updated dependencies [d147a13]
|
|
1878
|
+
- @object-ui/types@17.0.0
|
|
1879
|
+
|
|
3
1880
|
## 16.1.0
|
|
4
1881
|
|
|
5
1882
|
### Minor Changes
|
|
@@ -59,7 +1936,7 @@
|
|
|
59
1936
|
|
|
60
1937
|
Together with the `^15.1.1` alignment (#2662), a renderer CEL predicate now
|
|
61
1938
|
reaches the identical verdict as the server — including the framework's
|
|
62
|
-
`dateField == today()` equality fix (objectstack-ai/
|
|
1939
|
+
`dateField == today()` equality fix (objectstack-ai/objectstack#3205) once it
|
|
63
1940
|
lands in a published 15.x. The broader home-grown-vs-canonical divergence
|
|
64
1941
|
motivation is #2661.
|
|
65
1942
|
|
|
@@ -148,7 +2025,7 @@
|
|
|
148
2025
|
and removes both bounds together. A new `drillUrlFilters` module owns the
|
|
149
2026
|
write/read serialization so both sides can't drift (round-trip tested).
|
|
150
2027
|
|
|
151
|
-
Companion to the framework analytics change (objectstack-ai/
|
|
2028
|
+
Companion to the framework analytics change (objectstack-ai/objectstack#3256).
|
|
152
2029
|
|
|
153
2030
|
### Patch Changes
|
|
154
2031
|
|
|
@@ -163,7 +2040,7 @@
|
|
|
163
2040
|
(`visibleWhen`/`readonlyWhen`/`requiredWhen` via `fieldRules.ts`, which delegates
|
|
164
2041
|
to `@objectstack/formula`'s `ExpressionEngine`) now tracks the 15.x engine — and
|
|
165
2042
|
will pick up the framework's `dateField == today()` equality fix
|
|
166
|
-
(objectstack-ai/
|
|
2043
|
+
(objectstack-ai/objectstack#3205) automatically at the next 15.x release via the
|
|
167
2044
|
caret range. Renderer/action `visible`/`disabled` predicates are unaffected (they
|
|
168
2045
|
use the home-grown JS evaluator — tracked separately in #2661).
|
|
169
2046
|
|
|
@@ -736,7 +2613,7 @@
|
|
|
736
2613
|
(plugin-charts) and `DatasetWidget` (plugin-dashboard) both use it, so
|
|
737
2614
|
multi-dimension charts render consistently as grouped/coloured bars.
|
|
738
2615
|
|
|
739
|
-
Refs objectstack-ai/objectui#1759, objectstack-ai/
|
|
2616
|
+
Refs objectstack-ai/objectui#1759, objectstack-ai/objectstack#1890
|
|
740
2617
|
|
|
741
2618
|
- 7c239fd: Add `ComponentRegistry.unregister(type, namespace?)` — the inverse of
|
|
742
2619
|
`register()`. Clears the namespaced key and the bare-name fallback (when it
|