@object-ui/plugin-list 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 CHANGED
@@ -1,5 +1,863 @@
1
1
  # @object-ui/plugin-list
2
2
 
3
+ ## 17.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c5ccbd5: Stop declaring 12 `@object-ui/data-objectstack` / `@object-ui/plugin-chatbot` /
8
+ `@object-ui/plugin-list` symbols under names `@objectstack/spec` owns
9
+ (objectui#3160, objectstack#4115 batch 6). All three packages leave the ledger.
10
+
11
+ **Breaking for importers of `@object-ui/data-objectstack`** — four exported
12
+ names changed, because the spec exports the same name for a _different_ thing:
13
+
14
+ | was | now | what the spec's same-named export actually is |
15
+ | :-------------------- | :-------------------------- | :--------------------------------------------------------------------------------------- |
16
+ | `CacheStats` | `MetadataCacheStats` | the platform `ICacheService` counters (`keyCount`, `memoryUsage`) |
17
+ | `MetadataSaveOptions` | `MetadataClientSaveOptions` | options for writing a metadata item to a **file** (`format`, `path`, `indent`, `atomic`) |
18
+ | `SecurityPolicy` | `SecurityManagerPolicy` | the package supply-chain policy (`autoScan`, licences, code signing, sandbox) |
19
+ | `ValidationError` | `DataApiValidationError` | a plain `{ field, message, code? }` entry in a validation report |
20
+
21
+ Each pair is disjoint or nearly so — `MetadataSaveOptions` and `SecurityPolicy`
22
+ share not one key with the spec type whose name they wore — so none of them was
23
+ a dialect to reconcile; they were four unrelated concepts squatting on spec
24
+ names. `DataApiValidationError` follows the `<what was validated>Validation<Error|Result>`
25
+ convention registered on objectstack#4115 (`@object-ui/core` took
26
+ `SchemaNodeValidationError` in batch 4). Its **runtime** `name` deliberately
27
+ stays `'ValidationError'`: `normaliseClientError` and `@object-ui/react`'s
28
+ error-message helper both sniff `err.name`, so that string is a wire contract,
29
+ not a symbol.
30
+
31
+ **Breaking for importers of `@object-ui/plugin-chatbot`** — `PendingActionRow`
32
+ and `PendingActionStatus` are now re-exported from `@objectstack/spec/contracts`
33
+ instead of hand-transcribed, which narrows them. The copies had drifted three
34
+ ways, and each drift had **disabled a compile-time check** rather than merely
35
+ differed from one:
36
+
37
+ - `status: PendingActionStatus | string` — a union with `string` absorbs the
38
+ literals, so that annotation carried no information at all;
39
+ - `[key: string]: unknown` — the objectstack#4075 mechanism: an index signature
40
+ makes every structural comparison against the spec answer "identical", however
41
+ far the copy has drifted;
42
+ - `created_at` / `updated_at`, which the service contract does not carry and no
43
+ consumer in this repo reads.
44
+
45
+ **Breaking for importers of `@object-ui/plugin-list`** — `ViewTab` is derived from the spec's `ViewTabSchema`
46
+ — from its **input** side, because `pinned` / `isDefault` / `visible` carry
47
+ `.default()`s and this component is handed authored metadata, not parsed output.
48
+ That removes a renderer-side tolerance the copy carried: `visible` accepted
49
+ `string | boolean` and the tab bar compared it against the literal `'false'`, a
50
+ spelling no producer emits. `label` also stops being required (the spec makes it
51
+ optional; `name` is the identifier) and `filter` stops being `any`.
52
+
53
+ `ListView` and `UserFilters` keep their names as declared dialects: both are the
54
+ React **renderers** of the spec types whose names they share, and each takes that
55
+ spec type as a prop (`ListViewProps.schema`, `UserFiltersProps.config`) rather
56
+ than restating its shape. `Tool` and `MessageContent` in `plugin-chatbot` are
57
+ vendored Vercel AI Elements / Shadcn primitives — upstream's component API, not
58
+ objectui's authored surface — so the guard now skips that directory the same way
59
+ it already skips `components/src/ui/`, with a test that fails if any file there
60
+ stops carrying its vendor banner.
61
+
62
+ Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
63
+ tracks `@objectstack`, so breaking changes of our own ship as minor with the
64
+ semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
65
+ all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.
66
+
67
+ - 5cb75b3: fix(timeline,list): the timeline honours `timeline.dateField`, not just `timeline.startDateField` (#3129)
68
+
69
+ `dateField` is the pre-#2231 alias for `startDateField`. `@object-ui/types`
70
+ declares it on the nested config (`ListViewTimelineConfig`), and both
71
+ `ObjectView` read-sites (app-shell and plugin-view) resolve it — but the two
72
+ read-sites that actually drive the axis did not:
73
+
74
+ - `ObjectTimeline` consulted the alias only on the FLAT prop (`schema.dateField`),
75
+ never on the nested `schema.timeline`.
76
+ - `ListView` resolved it out of `options.timeline` but not out of the
77
+ spec-canonical `schema.timeline` — including in the capability gate, so such a
78
+ view could fail to offer the Timeline option at all.
79
+
80
+ So a view authored as `timeline: { dateField: 'start_date' }` — the spec nesting
81
+ with the legacy key — fell through to the caller's default (`created_at` /
82
+ `due_date`). That field is normally absent from the `$select` projection, so
83
+ every record came back without it and the timeline rendered all of them under
84
+ **No date** — while the configured date was sitting in the row untouched. That
85
+ also explains why widening the view's projection changed nothing: the projection
86
+ already carried the right field; the renderer was reading a different one.
87
+
88
+ Both read-sites now resolve the alias in the same precedence position they
89
+ already use for `options.timeline.dateField`. The spec key still wins wherever
90
+ both appear. Observable rendering change (records move out of "No date" into
91
+ real date buckets), hence `minor`.
92
+
93
+ ### Patch Changes
94
+
95
+ - 335041c: Stop declaring 13 `@object-ui/core` symbols under names `@objectstack/spec` owns
96
+ (objectui#3158, objectstack#4115 batch 4).
97
+
98
+ **Breaking for importers of `@object-ui/core`** — seven exported names changed,
99
+ because the spec exports the same name for a _different_ thing:
100
+
101
+ | was | now | what the spec's same-named export actually is |
102
+ | :----------------------- | :-------------------------------- | :--------------------------------------------------------------------------- |
103
+ | `ChartSeries` | `ChartSeriesBinding` | the authored dataset-binding descriptor (a measure `name`, no `data`) |
104
+ | `ActionHandler` | `ActionRunnerHandler` | the SERVER-side objectql handler, `(ctx) => unknown` |
105
+ | `PluginDefinition` | `RegistryPluginDefinition` | the platform PACKAGE manifest (`id`/`slug`/`staticPath`/install hooks) |
106
+ | `ValidationError` | `SchemaNodeValidationError` | plugin-manifest validation, keyed by `field`, no severity |
107
+ | `ValidationResult` | `SchemaNodeValidationResult` | ditto, with both arrays optional |
108
+ | `defineView` | `defineSystemView` | the VIEW-DOCUMENT factory: parses a `ViewSchema`, returns a validated `View` |
109
+ | `resolveCrudAffordances` | `resolveEffectiveCrudAffordances` | the object-level affordance matrix, with no notion of server API operations |
110
+
111
+ The other six keep their names and are now **imported from the spec** instead of
112
+ re-declared: `StyleMap`, `ResponsiveStyles` (ADR-0065), `RowHeight`,
113
+ `CONTEXT_TOKENS`, `CrudAffordances`, `RowCrudPredicates`.
114
+
115
+ **The copies were live misdescriptions, not just duplicates.** Three said so in
116
+ their own comments:
117
+
118
+ - `CONTEXT_TOKENS` carried a note that the duplication was "temporary until the
119
+ next coordinated release… because the installed `@objectstack/spec` predates
120
+ that export". The installed spec (17.0.0-rc.0) exports it, and the copy was
121
+ byte-identical — so it passed every value comparison and every behavioural
122
+ test for the whole interval in which its stated reason was false.
123
+ - `RowHeight` advertised itself as "the spec's `RowHeightSchema` vocabulary"
124
+ while being a hand-written union. It happened to be correct; nothing would
125
+ have caught the day it stopped being.
126
+ - `managedBy.ts` described itself as a "UI-side mirror of the framework's
127
+ `resolveCrudAffordances()`" and carried its own `DEFAULTS` table — a
128
+ line-for-line copy of the spec's `CRUD_AFFORDANCE_DEFAULTS`, plus a copy of
129
+ its override parser.
130
+
131
+ `resolveEffectiveCrudAffordances` now **delegates** the bucket/`userActions` half
132
+ to the spec's `resolveCrudAffordances()`, so the bucket table has exactly one
133
+ definition on the platform. What stays objectui's is the part the spec has no
134
+ notion of: intersecting that matrix with the server-resolved effective API
135
+ operation set (#3391), so the UI never offers a button the server would 405 —
136
+ and the name now says that instead of claiming to be the spec's function.
137
+
138
+ Deriving `RowCrudPredicates` also **tightens** it: the local copy typed
139
+ `visibleWhen`/`disabledWhen` as `unknown`, where the spec types them as
140
+ `Expression | ExpressionInput`. That was imprecision, not a deliberate dialect.
141
+
142
+ - 5eaa861: `list-view` and `embeddable-form` get a data source on the registry path — their required `objectName` was binding to nothing (#3144).
143
+
144
+ `SchemaRenderer` puts the data source on `SchemaRendererContext` and **never** injects it into
145
+ component props. A component that reads `props.dataSource` therefore needs its registration to
146
+ bridge the two. `object-form`, `object-kanban` and `object-calendar` each register a small
147
+ renderer that does exactly that. These two did not:
148
+
149
+ - `list-view` (and its `view:list` alias) registered the bare `ListView`, which reads
150
+ `props.dataSource` — so its `getObjectSchema` effect returned immediately, nothing was ever
151
+ fetched, and it rendered the `empty-state` "Nothing here".
152
+ - `embeddable-form`'s renderer was `({ schema }) => <EmbeddableForm config={schema} />`, dropping
153
+ the context entirely — so the read-only source it derives for its inner `ObjectForm` was never
154
+ built, and its submit path (`if (dataSource) await dataSource.create(...)`) had nothing to call.
155
+
156
+ Both declare `objectName` **required** in their registry `inputs`. A binding the protocol obliges
157
+ an author to supply, that nothing on that path can consume, is objectstack#4413's shape one layer
158
+ up — and the reason it went unnoticed is that the console never takes this path: it reaches
159
+ ListView through `ObjectView`'s `renderListView` render-prop, which passes a data source itself.
160
+ Broken on the registry/SDUI path, which is the path `sdui.manifest.json` describes and a
161
+ `kind:'react'` page walks.
162
+
163
+ Found by `apps/console/src/__tests__/public-block-binding-reach.test.tsx` (objectstack#4472), not
164
+ by hand — that suite mounts every public block declaring an `objectName` under a recording
165
+ `dataSource` and asserts the binding arrives. Its ledger carried these two as named debt; with the
166
+ bridge in place the ledger's both-directions assertion **failed until the entries were deleted**,
167
+ which is the mechanism working as designed. Only `record:related_list` remains, and legitimately
168
+ (it needs a parent record id from `RecordContext` before it may fetch).
169
+
170
+ An explicit `dataSource` prop still wins, so hosts passing their own are unaffected, and
171
+ `ListViewRenderer` forwards refs so `ListViewHandle` still works through the registry.
172
+
173
+ ## 17.1.0
174
+
175
+ ### Minor Changes
176
+
177
+ - d21794c: fix(list,i18n): a 400 from the server no longer reads as "check your connection"
178
+
179
+ `classifyLoadError` was written because a 403 rendered the same
180
+ "check your connection and try again" panel as a genuine outage — its own doc
181
+ comment says users "were told to debug their network when the server had
182
+ (correctly) denied them access." It made that distinction for 401 and 403 and
183
+ then sent **everything else**, including 4xx, to the network branch.
184
+
185
+ A **400** is the server saying it understood the request and will never accept
186
+ it. Retrying resends the identical bad request, so "check your connection and
187
+ try again" is advice that cannot work — the same mistake the function exists to
188
+ prevent, one status code over.
189
+
190
+ This became reachable from ordinary stored metadata with
191
+ objectstack-ai/objectstack#4121: a `$filter` array that is not a filter AST is
192
+ now rejected at the protocol with `400 INVALID_FILTER`, where it previously
193
+ reached a driver (and, for a lone `['and']`, silently returned every row). A
194
+ view saved with such a filter now answers 400 on every load.
195
+
196
+ Adds a fourth classification, `rejected`, for `status === 400` and for the
197
+ server's 400-class codes (`INVALID_FILTER`, `UNSUPPORTED_QUERY_PARAM`,
198
+ `INVALID_QUERY`). Its copy points at the filter rather than the network, and
199
+ says who can fix it when the view is saved that way. 403/401 keep priority, so a
200
+ permission denial can never read as a bad request — pinned by a test.
201
+
202
+ The two new strings are added to **all ten locale packs**, not just `en`: the
203
+ neighbouring panels are translated, and `fallbackLng: 'en'` would have rendered
204
+ this one in English beside them. The full-parity gate
205
+ (`all-locales-key-parity.test.ts`) caught the pack I missed.
206
+
207
+ Verified: 5 new tests — numeric status, error code without a status, a status
208
+ embedded in the message text, and the 403/401 ordering guard. Reverting the
209
+ branch fails four of them. `plugin-list` + `i18n`: **403 tests across 29 files**,
210
+ green.
211
+
212
+ - c4db402: refactor(views): ListView's `aria` and `sharing` are the spec sub-shapes (#2890 scope A step 5)
213
+
214
+ Last rename batch in the ListView vocabulary migration.
215
+
216
+ **`aria`** is now the spec's `AriaPropsSchema`: `label` → `ariaLabel`,
217
+ `describedBy` → `ariaDescribedBy`, folded at the ListView boundary like every
218
+ other legacy key. Two things fall out of adopting the spec shape:
219
+
220
+ - `role` becomes authorable. The list region hardcoded `role="region"`; it now
221
+ reads `aria.role` and falls back to `region`.
222
+ - `aria.live` stays as a documented local extension — it has no spec
223
+ counterpart, and dropping it would silently disable a shipped capability.
224
+ Promote it rather than growing that extension.
225
+
226
+ **`sharing`** is now the spec's `ViewSharingSchema` (`{ type, lockedBy }`),
227
+ imported by reference — the local four-key object is gone. The legacy pair folds
228
+ in: `visibility` collapses onto the two ownership kinds the spec models (only
229
+ `private` is `personal`; `team` / `organization` / `public` are all
230
+ `collaborative`), and a bare `enabled: true` maps to `personal`, which is the
231
+ badge the user already saw (the old title fell back to `'private'`).
232
+
233
+ _Visible change_: the share badge's tooltip shows the spec ownership type, so a
234
+ view authored with `visibility: 'team'` reads "Sharing: collaborative" instead
235
+ of "Sharing: team". The four-value audience has no spec home and nothing but
236
+ that tooltip consumed it; keeping a second audience enum alive would re-open the
237
+ fork this issue closes.
238
+
239
+ Also fixes the **spec bridge**, which was doing the opposite of its job: given a
240
+ spec-shaped `sharing`, `transformListView` _downgraded_ it — inventing a legacy
241
+ `visibility` audience and an `enabled` flag that the renderer then had to fold
242
+ back. Both sides speak `ViewSharing` now, so it passes through.
243
+
244
+ `conditionalFormatting` and `exportOptions` are deliberately **not** folded.
245
+ Both objectui shapes are supersets carrying capability the spec cannot express —
246
+ the `{ field, operator, value }` rule form, and `maxRecords` / `includeHeaders`
247
+ / `fileNamePrefix`. Folding them onto the narrower spec shapes would delete
248
+ working features; they want promotion upstream, not a rename.
249
+
250
+ - 5319bf1: feat(views): the list toolbar speaks one vocabulary — `userActions` (#2890 scope A step 3)
251
+
252
+ The seven bare `show*` toolbar flags fold into the spec's `userActions`, and the
253
+ renderer reads nothing else. `showDescription` folds into
254
+ `appearance.showDescription` at the same boundary.
255
+
256
+ | legacy | canonical |
257
+ | :-------------------------------------------------------- | :-------------------------------------------------------- |
258
+ | `showSearch` / `showSort` / `showFilters` / `showDensity` | `userActions.search` / `.sort` / `.filter` / `.rowHeight` |
259
+ | `showGroup` / `showHideFields` / `showColor` | `userActions.group` / `.hideFields` / `.rowColor` |
260
+ | `showDescription` | `appearance.showDescription` |
261
+
262
+ **The last three are new keys, and they close a capability hole rather than just
263
+ renaming one.** `@objectstack/spec`'s `UserActionsConfigSchema` documents itself
264
+ as "which interactive actions are available to users in the view toolbar — each
265
+ boolean toggles the corresponding toolbar element on/off", and already carries
266
+ `rowHeight` (objectui's `showDensity` under its spec name). Grouping, column
267
+ visibility and row coloring are the same kind of toggle: the spec models all
268
+ three as _configuration_ (`grouping`, `hiddenFields`, `rowColor`) but has no
269
+ "may the user change it" switch for any of them.
270
+
271
+ The consequence was visible in the product. With no `userActions` key to read,
272
+ the two list surfaces **hardcoded opposite policies**: `InterfaceListPage` (the
273
+ author-curated interface page) pinned all three OFF, `ObjectDataPage` pinned two
274
+ ON — and an interface-page author could not turn grouping back on for end users
275
+ at all. Both surfaces now express their policy as `userActions` defaults, which
276
+ an author can override.
277
+
278
+ Until the keys land in `@objectstack/spec`, `@object-ui/types` carries them as a
279
+ documented `.extend()` on `UserActionsConfigSchema` (the same shape
280
+ `ListColumnSchema` uses while waiting on objectstack#3761); it collapses into a
281
+ plain re-export once they do. Note the spec schema is not `.strict()`, so before
282
+ this an author writing `userActions: { group: false }` had it **silently
283
+ stripped** — valid on parse, no effect at render.
284
+
285
+ Defaults are unchanged and deliberately asymmetric, matching what these flags
286
+ have always done: `search` / `sort` / `filter` / `rowHeight` / `group` are on
287
+ unless turned off; `hideFields` / `rowColor` are off unless turned on. Making
288
+ them uniform would grow two buttons on every existing view, so it is left as its
289
+ own product decision rather than smuggled into a vocabulary migration.
290
+
291
+ Also drops a dead relay in app-shell's `ObjectView`, which forwarded
292
+ `showDescription` onto the node although `ListView` has only ever read
293
+ `appearance.showDescription`.
294
+
295
+ - 24e0e0a: feat(components,grid,list): a column-header sort orders the whole list, not the page you can see — #3106
296
+
297
+ Clicking a column header under server pagination sorted **the current page**.
298
+ The user saw "sorted by this column" and got "these fifty rows are in order;
299
+ page 2 starts over". The sort was real — its scope was not the one the screen
300
+ implied — and it had no way out of `data-table` at all: the sort lived in two
301
+ `useState`s with no callback, so the layer that issues the request could not
302
+ see it even in principle.
303
+
304
+ `DataTable` gains `manualSorting` + a controlled `sort` + `onSortChange`. In
305
+ that mode it sorts nothing, reports what a header click asks for, and renders
306
+ `sort` as the indicator — keeping **no** sort state of its own, because a
307
+ private copy beside a controlled prop is the shape the defect had.
308
+
309
+ `ObjectGrid` turns that into a `$orderby` in both of its server modes (its own
310
+ fetch, and a parent-driven one), and `ListView` lands it in `currentSort` — the
311
+ same state the toolbar's sort builder writes. One sort, two controls: that is
312
+ what makes "does a header sort outrank the saved view's sort?" a non-question
313
+ rather than a precedence rule someone has to remember.
314
+
315
+ Three details that are decisions, not incidentals:
316
+
317
+ - **A header click replaces the order** instead of appending to it, so the
318
+ column under the cursor is the one the list is sorted by. Multi-key orders
319
+ still come from the sort builder, and the headers render them numbered.
320
+ - **It cannot ask for "no sort".** In client mode the third click clears, and
321
+ that is meaningful there — the rows return to the order they arrived in.
322
+ Across a server-paged collection there is no such order (objectstack#4363), so
323
+ a header offering it would hand the user a worse lie than the one being fixed.
324
+ Clearing stays with the sort builder, which can restore the view's default.
325
+ - **Relational columns render no sort affordance** under server sorting. A
326
+ `lookup` column shows a related record's name while `$orderby` can only order
327
+ by the stored id (objectstack#4256) — the same reason #3096 removed them from
328
+ the toolbar's sort picker. Client-side sorting keys off the rendered label, so
329
+ those headers stay live there.
330
+
331
+ Client-side tables are untouched: same three-state cycle, same local sort.
332
+
333
+ ### Patch Changes
334
+
335
+ - 95b7214: fix(list,grid,detail,tree,core): every column resolver reads one key (#3104 PR2)
336
+
337
+ PR1 (#3119) put a canonicalizing fold at ListView's ingestion boundary. This
338
+ converges the 22 read sites themselves onto `columnIdentity()` from
339
+ `@object-ui/core`, so a surface that is NOT downstream of that fold resolves
340
+ the same identity anyway.
341
+
342
+ That distinction is the user-visible part. A standalone `object-grid` node —
343
+ authored directly on a page, with no `list-view` above it — never passed
344
+ through `normalizeListViewSchema`. Its `getSelectFields` read `c.field` alone
345
+ while the `ensureId` probe one line above read `f?.name || f?.field`, so a
346
+ legacy `{ name: 'account' }` column reached `$select` as a literal `undefined`
347
+ hole: the server never returned the field and every cell in that column came
348
+ back empty. Same for `ObjectTree`, `RelatedList` and the `record:details` /
349
+ `record:related_list` renderers.
350
+
351
+ Converged:
352
+
353
+ | Surface | Was | Now |
354
+ | ---------------------------------------- | ---------------------------------------------- | ----------------------------------- |
355
+ | `ListView` ×9 + its 2 request builders | `name \|\| fieldName \|\| field` vs `f?.field` | `columnIdentity()` |
356
+ | `RelatedList` ×8 | `accessorKey \|\| field \|\| name` | `accessorKey \|\| columnIdentity()` |
357
+ | `ObjectGrid` | name-first probe vs `c.field` projection | `columnIdentity()` |
358
+ | `ObjectTree` | `name \|\| fieldName \|\| field \|\| key` | `columnIdentity() \|\| key` |
359
+ | `buildExpandFields` | `field ?? name ?? fieldName` | `columnIdentity()` |
360
+ | `record-details` / `record-related-list` | `field \|\| name (\|\| key)` | `columnIdentity() (\|\| key)` |
361
+
362
+ `accessorKey` keeps its precedence in `RelatedList` — it is TanStack Table's
363
+ column key, not ObjectStack metadata identity, and only the `field || name`
364
+ tail was converged. `key` stays a tail fallback in `ObjectTree` and
365
+ `record-related-list` for the same reason: it is a generic entry key.
366
+
367
+ Two incidental fixes that TypeScript surfaced once the resolver stopped
368
+ returning `any`: ListView's filter-field options and its hide-fields popover
369
+ both built entries keyed `undefined` for a column with no resolvable identity.
370
+ Those entries could never match a column; they are now dropped.
371
+
372
+ **Inventory re-triage.** PR1 recorded 24 family members. Two were mis-classified
373
+ and are reclassified here rather than converged — reading what they actually
374
+ feed shows they are not column reads at all:
375
+
376
+ - `ViewPreview.tsx` adapts a ViewItem **form** section to what `object-form`
377
+ selects by (`field` → `name`) — the #3090 two-layer join.
378
+ - `SchemaForm.tsx` renders an arbitrary metadata **array** into a popover
379
+ summary and guesses at a display key; the entries are validations, actions,
380
+ or whatever the JSON schema declares.
381
+
382
+ So the family was 22, and it is now **0**. The ratchet asserts that, asserts
383
+ each converged surface actually routes through the shared reader (a surface
384
+ that dropped identity resolution instead of converging it goes red), and pins
385
+ `accessorKey`'s precedence in `RelatedList`.
386
+
387
+ - 9eb932b: fix(console): three real-user console failures — 403 blamed on the network, ⌘K search capped at 8 objects, nav gating fields inert
388
+
389
+ 1. **List error panel classifies the failure** (`plugin-list`, `i18n`): a 403/401 from the data source used to render the same "check your connection" copy as a genuine outage, sending users to debug their network while the server was correctly denying access. The panel now classifies by `httpStatus`/`status`/`statusCode`, the `PERMISSION_DENIED`/`UNAUTHORIZED` error codes, or an `HTTP <status>` message prefix, and renders dedicated permission-denied / sign-in-required copy (all nine locales).
390
+
391
+ 2. **⌘K / full-page search scope is no longer truncated** (`react`): `maxObjectsQueried` caps the per-object fanout fallback, not the search scope — it used to slice the candidate pool itself, so the `objects` whitelist sent to the platform's `/api/v1/search` only ever named the first 8 nav objects. Which sidebar group came first decided which records were findable; everything later in the nav was unsearchable no matter what the user typed.
392
+
393
+ 3. **Nav gating fields finally gate** (`app-shell`): `evaluateVisibility` only evaluated `${…}` template strings, so the `{ dialect: 'cel', source }` envelopes the spec normalizes every authored `visible` predicate into fell through to a blanket "visible" — a constant-false predicate still rendered for everyone. It now delegates to `ExpressionEvaluator.evaluateCondition`, which routes CEL envelopes to the canonical `@objectstack/formula` engine. And the sidebars' `requiredPermissions` check treats a bare name as an ADR-0066 system capability (union of the user's permission-set `systemPermissions` from `/me/permissions`) — the same subset rule the server applies to `AppSchema.requiredPermissions` — instead of misreading it as `can(<name>, 'read')`, which had degraded `requiredPermissions` into a hide-from-everyone switch (admins included). The `object:action` form and the legacy object-read fallback keep working.
394
+
395
+ - 7f0252e: fix(list,data-objectstack,types): exporting a searched list no longer downloads the unsearched superset
396
+
397
+ The server-streamed export mirrored the view's `filter` and `sort`, and the
398
+ code comment claimed that made the file match the screen:
399
+
400
+ > Mirrors the active view's filter + sort so the exported file matches what the
401
+ > user sees.
402
+
403
+ It mirrored one half. There was no way to carry the term a user had typed into
404
+ the search box — `ExportDownloadRequest` had no field for one — so exporting
405
+ during a search produced **more rows than the list showed**, in a file that
406
+ looks authoritative, with nothing indicating the difference. The client-side
407
+ fallback was always correct (it serializes the already-searched `data`); only
408
+ the server path was wrong, and it is the one that handles xlsx.
409
+
410
+ Same family as a dropped filter (objectstack#3948, objectstack#4181): a
411
+ plausible answer that is quietly broader than the one asked for.
412
+
413
+ - `ExportDownloadRequest` gains `search` / `searchFields`.
414
+ - `ObjectStackAdapter.exportDownload` sends them as `search=` / `searchFields=`,
415
+ trimming the term and omitting both when it is blank (`searchFields` alone
416
+ means nothing).
417
+ - `ListView` passes the active `searchTerm` and the view's `searchableFields`,
418
+ and both are now in the export callback's dependency array — a stale closure
419
+ would export the wrong row set.
420
+
421
+ Requires a server with objectstack#4230. Older servers ignore unknown query
422
+ params on this route, so they keep today's behaviour rather than erroring.
423
+
424
+ **Also: the filter merge is no longer written twice.** The three filter sources
425
+ (view filter, filter-panel group, per-field user filters) were merged by
426
+ verbatim copies in the data fetch and in the export — two copies that must
427
+ agree, deciding respectively what the user _sees_ and what they _download_.
428
+ Both now call `buildEffectiveFilter`. This is a pure extraction: the copies did
429
+ agree, and the four parity tests added for it pass against the old code too.
430
+ They exist to keep it that way — the adapter's duplicated filter-shape check
431
+ had already drifted apart unnoticed (#3072).
432
+
433
+ - c4d7b20: fix(view,list,core): a view's filter no longer disappears, or arrives as a predicate on columns that don't exist
434
+
435
+ Sweeping the other `$filter` producers after #3078 turned up two live defects in
436
+ `ObjectView`, which fetches its own data for calendar / kanban / gallery /
437
+ timeline (grid delegates to `ObjectGrid`).
438
+
439
+ **1. An object filter was dropped, and only for non-grid views.**
440
+ `table.defaultFilters` is declared `Record<string, any>`, and the merge tested
441
+ `baseFilter.length > 0` — `undefined > 0` for an object. So the filter vanished
442
+ and the view returned **every record**. `ObjectGrid` assigns the same value
443
+ straight to `params.$filter`, so one view definition filtered correctly as a
444
+ grid and returned everything as a calendar.
445
+
446
+ **2. Rule objects were spread into the `and`, not wrapped.**
447
+ `['and', ...baseFilter, ...userFilter]` is only correct when the source is an
448
+ array of AST nodes. `activeView.filter` is a spec `ViewFilterRule[]`, so
449
+ spreading put bare rule objects where the AST expects nodes:
450
+
451
+ ```js
452
+ isFilterAST([
453
+ "and",
454
+ { field: "stage", operator: "eq", value: "won" },
455
+ ["owner", "=", "me"],
456
+ ]);
457
+ // false → 400 since objectstack#4121
458
+ parseFilterAST(same);
459
+ // {$and:[{field:'stage',operator:'eq',value:'won'}, {owner:'me'}]}
460
+ ```
461
+
462
+ That second line is a predicate over three columns named `field`, `operator`
463
+ and `value` — which don't exist.
464
+
465
+ > **Correction.** The first version of this note said the spread was "reachable
466
+ > whenever a view with a filter meets a user filter value". That was wrong for
467
+ > `ObjectView`: the branch required a non-empty user filter, and nothing ever
468
+ > wrote the state it was built from, so it could never run. The shape is
469
+ > genuinely broken — a live server answers it with a 400 — and the adapter-level
470
+ > defence added alongside is still warranted for any producer that emits it, but
471
+ > **this particular site was dead code, not a live defect.** Defect 1 above was
472
+ > live: it sat on the always-taken path. The dead machinery behind the wrong
473
+ > claim is removed in a follow-up.
474
+
475
+ New in `@object-ui/core`: `toFilterNode` normalizes one source (rule array / AST
476
+ / MongoDB object) and `mergeFilterNodes` combines sources as siblings under one
477
+ `and`. `ObjectView` and `ListView.buildEffectiveFilter` both use them, so the
478
+ three filter shapes are reconciled in one place instead of by hand at each
479
+ renderer.
480
+
481
+ `ObjectStackAdapter` also now translates a bare rule object sitting directly
482
+ under a logical node — the chokepoint defence for any producer still emitting
483
+ the spread shape. Only rule-_shaped_ objects are touched; a child with no
484
+ `field` is a genuine MongoDB condition and passes through untouched.
485
+
486
+ **Correcting a comment shipped in #3078.** `buildEffectiveFilter` documented the
487
+ dropped-object case as unreachable, "nothing in this repo produces one for a
488
+ list view". That was wrong: `ObjectView` passes `mergedFilters` straight into
489
+ that schema's `filter`, and its last fallback is `table.defaultFilters`. The
490
+ case is now handled rather than explained away.
491
+
492
+ Verified with 19 tests across the four packages; reverting each source file
493
+ fails the ones that cover it. Emitted filters are asserted against the spec's
494
+ own `isFilterAST` / `parseFilterAST`, including an executable pin on what the
495
+ old spread shape produced.
496
+
497
+ - 2d5d594: fix(list,detail): sorting a lookup column no longer orders by an invisible key — #3096
498
+
499
+ A relational column (`lookup` / `master_detail` / `user` / `tree`) never holds
500
+ the string its cell shows: it holds the `$expand`-ed record, or a raw foreign-key
501
+ id whose label was resolved separately. Every sort path took that raw value as
502
+ its key, so the column of names came back in an order with no relation to the
503
+ names — sorting looked broken, with nothing saying the key was something else.
504
+
505
+ The two halves are fixed differently, because they can order by different things:
506
+
507
+ - **Client-side sorts** (grid column headers, any `data-table`, a non-windowed
508
+ related list) now key off the label the cell renders, via the new
509
+ `getSortValue` / `compareSortValues` in `@object-ui/core` — which resolves an
510
+ expanded record through `getRecordDisplayName` (ADR-0079), so the sort key and
511
+ the lookup cell agree on which field names a record. This replaces two broken
512
+ comparators: `a[col] < b[col]` is always false between two objects (the
513
+ comparator collapsed to a constant and permuted the rows), and
514
+ `String(a[col])` is `"[object Object]"` (every row compared equal, so the sort
515
+ silently did nothing).
516
+ - **Server `$orderby` sorts** cannot be fixed here — the key is the stored id by
517
+ construction, and `objectstack#4256` settled that no relation join is coming.
518
+ So those entry points stop offering the illusion: the ListView toolbar sort
519
+ picker withholds relational fields and explains why (pointing at a formula
520
+ field as the supported way to sort by a related name), and a windowed related
521
+ list renders no sort button for them.
522
+
523
+ A relational field the view's CURRENT sort already uses stays listed, labelled
524
+ `(by ID)`, so view metadata authored or saved with such a sort round-trips
525
+ instead of rendering a blank row and losing the sort on the next edit.
526
+
527
+ ## 17.0.0
528
+
529
+ ### Minor Changes
530
+
531
+ - 2735de6: feat: render the server's effective API operation set (#3391 PR-4)
532
+
533
+ The frontend now consumes the per-object **effective API operation set** the
534
+ server resolves (from `/me/permissions` `apiOperations`, framework #3391) —
535
+ never the raw `apiMethods` — so Import/Export/New/Edit/Delete buttons match what
536
+ the server will actually admit, and a 405 import refusal shows a dedicated
537
+ message instead of silently falling back.
538
+
539
+ - **core** `resolveCrudAffordances(obj, effectiveApiOperations?)` — new optional
540
+ second argument intersects each affordance bit with its API operation
541
+ (create/import→create/import, edit→update, delete→delete, exportCsv→export).
542
+ Omitting it (old backend / no effective set) leaves affordances unchanged.
543
+ - **permissions** — `/me/permissions` response carries per-object
544
+ `apiOperations`; `PermissionContextValue.getObjectApiOperations(object)`
545
+ exposes it (undefined when absent → callers keep current behavior); `check()`
546
+ maps `import→allowCreate`, `export→allowRead`.
547
+ - **app-shell** `ObjectView` intersects its toolbar affordances with the object's
548
+ effective operations (Import); the platform-admin identity-import bypass is
549
+ unaffected.
550
+ - **plugin-list** `ListView` / **plugin-grid** `ObjectGrid` gate the Export
551
+ button (and export handler) on effective `export`; `plugin-grid` gains the
552
+ `@object-ui/permissions` workspace dependency.
553
+ - **plugin-grid** `ImportWizard` — a 405 / `OBJECT_API_METHOD_NOT_ALLOWED`
554
+ import refusal is detected by a new `isImportNotAllowed` predicate at every
555
+ catch site (async, sync, dry-run) and STOPS with a dedicated
556
+ `grid.import.notAllowed` message (10 locales + fallback dict) — it never falls
557
+ back to the sync/legacy path (which 405s too), distinct from the 404
558
+ route-absent fallback.
559
+
560
+ Backward-compatible: a missing effective set (unrestricted object, older
561
+ backend, or no permission provider) preserves the current default-allow
562
+ behavior everywhere.
563
+
564
+ - ba45145: feat: gate list row Edit/Delete and bulk delete on the server's effective operation set (#3720)
565
+
566
+ The **fourth** surface #3391 left open. The three earlier rounds — the toolbar
567
+ (objectui#2823), detail/form (#3546, objectui#2832 + #2876) and related lists
568
+ (#3546) — all route through `resolveCrudAffordances`. The main list's **row
569
+ CRUD** does not: it has its own resolver (`plugin-grid`'s
570
+ `resolveRowCrudAffordances`), so none of those rounds ever reached it.
571
+
572
+ Its gate was `operations ?? { update: !!onEdit, delete: !!onDelete }` — and
573
+ `ObjectView` wires `onEdit`/`onDelete` unconditionally while view JSON rarely
574
+ declares `operations`, so it was effectively always-on. A caller whose effective
575
+ set carried neither `update` nor `delete` still got the row kebab's Edit/Delete
576
+ **and** the bulk delete, the most destructive affordance on the list.
577
+
578
+ - **plugin-grid** `resolveRowCrudAffordances` now takes `managedBy` and
579
+ `effectiveApiOperations` and resolves the object verdict through the shared
580
+ `resolveCrudAffordances` policy — so the row gate is the SAME decision the
581
+ toolbar, record header, form and related lists make. It also returns
582
+ `objectCanDelete`, the object-level delete verdict that bulk delete gates on
583
+ (bulk rides `onBulkDelete`, a different callback from the row `onDelete`).
584
+ - **plugin-grid** `ObjectGrid` threads its existing `effectiveApiOps` — until
585
+ now fed only to Export — into the row gate, and applies the delete verdict to
586
+ bulk delete: the implicit `['delete']`, an author-declared
587
+ `bulkActions: ['delete']`, and any `bulkActionDefs` entry with
588
+ `operation: 'delete'`. A declared bulk action is a _wiring_ declaration, not a
589
+ permission grant. Custom action ids and non-delete operations pass through
590
+ untouched.
591
+ - **plugin-list** `ListView`'s own bulk bar (the non-grid views — kanban /
592
+ calendar / gallery; the grid path delegates to `ObjectGrid`) drops its
593
+ built-in `delete` under the same verdict.
594
+
595
+ Also closes the ADR-0103 gap on this chain: `rowCrudAffordances` documented the
596
+ bucket lock as "applied upstream via the view's `operations.*`", but the
597
+ all-open default meant it never was — an engine-owned `system` / `append-only` /
598
+ `better-auth` object leaked a generic row Edit/Delete that the engine rejects
599
+ (`assertEngineOwnedWriteAllowed`). Running the shared policy applies it, and a
600
+ `userActions` opt-in still re-opens it (e.g. `sys_user`'s `edit`).
601
+
602
+ Same semantics as the earlier rounds: **intersection, never union** — a server
603
+ grant cannot re-open what the bucket or `userActions` closed, and a
604
+ `userActions` opt-in cannot survive a server denial. A missing effective set
605
+ (unrestricted object, older backend, or no `PermissionProvider`) preserves the
606
+ current behavior.
607
+
608
+ - cd09a7b: refactor(views): ListView reads the spec-canonical `columns`, with legacy `fields` folded in one normalizer (#2890 scope A step 1)
609
+
610
+ `ListViewSchema` has been derived from `@objectstack/spec/ui` since #2231, but
611
+ the renderer still spoke objectui's own vocabulary for the same concepts. First
612
+ rename closed: **`fields` → `columns`**.
613
+
614
+ Legacy acceptance does not disappear — stored view metadata in user databases
615
+ carries `fields` — but it now lives in exactly one place instead of being
616
+ re-implemented per read-site:
617
+
618
+ - **New `normalizeListViewSchema` (`@object-ui/core`)** folds `fields` into
619
+ `columns` (canonical wins when both are present) and drops the legacy key, so
620
+ a read-site that was missed fails loudly instead of quietly taking the legacy
621
+ path. It also absorbs the `viewType` renderability default ListView applied
622
+ inline. Non-mutating, idempotent, and returns its input by reference when
623
+ there is nothing to fold, so ListView's downstream memos keep a stable
624
+ dependency identity.
625
+ - **`ListView` normalizes once at the component boundary**, before anything
626
+ reads the schema. This is what guarantees the fold runs: nothing on the render
627
+ path parses view metadata through zod (the zod schemas serve the CLI
628
+ validator, the VS Code extension and tests), so a `z.preprocess` on
629
+ `ListViewSchema` — spec-side or local — would never execute.
630
+ - **Producers emit `columns`**: `ObjectView`'s `renderListView` payload,
631
+ `ObjectDataPage`, `InterfaceListPage` and the `list-view` registry defaults
632
+ had been _downgrading_ already-canonical `columns` config back to `fields`.
633
+
634
+ Two latent inconsistencies go away with it: the filter builder's
635
+ objectDef-not-loaded fallback now resolves `ListColumn.field` (it read only
636
+ `name`/`fieldName`, so object-form columns produced unnamed filter entries), and
637
+ the column list no longer depends on which of the two keys a host happened to
638
+ emit.
639
+
640
+ `fields` stays declared on `ListViewSchema` and in the drift guard's sanctioned
641
+ set — it is still valid input, and `@objectstack/spec`'s `react-blocks.ts`
642
+ sanctions it as the React-tier `<ListView fields>` prop — but it is input-only.
643
+
644
+ - f1abf0e: fix(views): ListView reads the spec-canonical `filter`, so a view's base filter reaches every visualization (#2890 scope A step 4)
645
+
646
+ Third rename in the ListView vocabulary migration: **`filters` → `filter`**. Unlike
647
+ the first two this closes a live bug, because the fork was asymmetric.
648
+
649
+ `ListView` was the **only** surface in the repo reading `filters`. Every child
650
+ view — `ObjectGrid`, `ObjectGallery`, `ObjectKanban`, `ObjectCalendar`,
651
+ `ObjectGantt`, `ObjectMap`, `ObjectTree`, `ObjectChart` — reads `filter`, and
652
+ `ListView` handed them `filters`. Wherever a child fetches its own rows instead
653
+ of receiving `ListView`'s, the view's base filter was silently dropped:
654
+
655
+ - **a `chart` list view aggregated the whole object.** The chart branch built an
656
+ `object-chart` node with `filters:`; `ObjectChart` reads `schema.filter` and
657
+ never read `filters`, so a chart view with a base filter charted unfiltered
658
+ totals.
659
+ - the same applied to any of the other view components rendered standalone from
660
+ a list-view-shaped config.
661
+
662
+ Conversely, a **spec-authored** list view — one carrying `filter`, which is what
663
+ the spec says and what `runtime-metadata-persistence` and "Save as view" already
664
+ persist — rendered **unfiltered** in `ListView`, because nothing read that key.
665
+
666
+ The fold is a key rename only. Both keys carry an ObjectQL FilterNode array
667
+ everywhere in objectui; every consumer passes the value straight to `$filter`.
668
+ (The spec types `filter` as `ViewFilterRule[]` — `{field, operator, value}`
669
+ objects — so objectui's field is typed from the spec but used as something else.
670
+ That mismatch is real and left alone here: converting formats inside a
671
+ vocabulary fold would change what reaches the data source.)
672
+
673
+ Also collapses a duplicated computation in `app-shell`'s `ObjectView`, which
674
+ computed the same effective filter **twice** — once as `filter` for the child
675
+ views, once as `filters` for `ListView` — with the two copies subtly different
676
+ (only one fell back to `listSchema.filter`; only the other ran token
677
+ substitution over the URL filters). There is now one computation, keeping both
678
+ behaviors.
679
+
680
+ `filters` stays declared on `ListViewSchema` and in the drift guard's sanctioned
681
+ set — stored views carry it and it is still valid input — but it is input-only.
682
+
683
+ - f05b84e: refactor(views): ListView resolves density from the spec-canonical `rowHeight` (#2890 scope A step 2)
684
+
685
+ Second rename in the ListView vocabulary migration: **`densityMode` → `rowHeight`**,
686
+ folded in the same `normalizeListViewSchema` that step 1 introduced.
687
+
688
+ Unlike `fields`/`columns` this is not a pure alias — the two vocabularies are
689
+ different sizes. The spec has five row heights (`compact`/`short`/`medium`/
690
+ `tall`/`extra_tall`); ListView's toolbar offers three densities
691
+ (`compact`/`comfortable`/`spacious`). Both directions now live in one place as
692
+ `DENSITY_MODE_TO_ROW_HEIGHT` / `ROW_HEIGHT_TO_DENSITY_MODE`, chosen so a fold
693
+ followed by a read is a round trip (`spacious` → `tall` → `spacious`), with the
694
+ narrowing collapse (`short` → `compact`, `extra_tall` → `spacious`) stated once
695
+ instead of being re-derived per call site.
696
+
697
+ Two behavior fixes fall out of it:
698
+
699
+ - **Precedence is no longer inverted.** `ListView` read `densityMode` _first_, so
700
+ a view carrying both keys rendered the legacy value — backwards from every
701
+ other legacy/canonical pair in the schema. The canonical key now wins.
702
+ - **The toolbar stops re-seeding the legacy key.** `ObjectView`'s
703
+ `onDensityChange` persisted `densityMode` into stored view metadata on every
704
+ density toggle, so the legacy vocabulary kept regrowing underneath the
705
+ migration. It persists `rowHeight` now.
706
+
707
+ `densityMode` stays declared on `ListViewSchema` and in the drift guard's
708
+ sanctioned set — stored views carry it and it is still valid input — but it is
709
+ input-only.
710
+
711
+ ### Patch Changes
712
+
713
+ - ab46110: fix(list): show the real match total in the record-count status bar under server pagination
714
+
715
+ The Airtable-style record-count bar read `data.length`, but under server-side
716
+ pagination (#2212) `data` is only the current page window — so a 158-row result
717
+ paginated 100/page reported "100 条记录" on page 1 and "58 条记录" on page 2,
718
+ never the true total. There was no other place to see how many records the
719
+ query matched.
720
+
721
+ The bar now shows the server's grand total (`serverTotal`) when known, falling
722
+ back to `data.length` when the whole result set is in memory (non-paginated,
723
+ grouped and non-grid views are unchanged — `serverTotal` is null there, so the
724
+ count is identical to before). Browser-verified against the showcase contacts
725
+ list: the bar reads "158 条记录" and stays stable across pages, and switching to
726
+ grouped/other views correctly resets to the loaded count.
727
+
728
+ - 7d46648: fix(hooks): stop calling translation hooks inside try/catch (objectui#2879)
729
+
730
+ Eleven call sites wrapped a React hook in `try`/`catch` to make it
731
+ "provider-safe". `useObjectTranslation` and `useObjectLabel` already are — they
732
+ read context optionally and fall back to react-i18next's global instance, and
733
+ never throw. The `catch` bought nothing and cost correctness: a throw _after_
734
+ the hook ran desyncs hook order on the next render, because React matches hooks
735
+ positionally. objectui#2595/#2596 fixed exactly this in `@object-ui/i18n`'s
736
+ `createSafeTranslation`; nine plugin-local re-implementations kept their own
737
+ copy of the bug, and two more (`ObjectTimeline`, `ObjectView`) were found by the
738
+ new lint rule below — `ObjectView` had even suppressed
739
+ `react-hooks/rules-of-hooks` inline to keep it.
740
+
741
+ - Six exact re-implementations now delegate to `createSafeTranslation`:
742
+ `plugin-detail`, `plugin-timeline`, `plugin-list`, `plugin-calendar`,
743
+ `plugin-grid`'s `ObjectGrid`, `plugin-designer`.
744
+ - `components`' `data-table` also delegates; `createSafeTranslation` now
745
+ returns `language` alongside `t` so consumers that localize dates don't need
746
+ a second hook call. Purely additive.
747
+ - `plugin-gantt` and `plugin-grid`'s `ImportWizard` keep their local hooks —
748
+ they fall back _per key_, which a single-probe factory cannot express and
749
+ which their comments justify (a host dictionary that covers common keys but
750
+ lags on newer ones). Only the `try`/`catch` is removed.
751
+ - `ObjectTimeline` and `ObjectView` call the hook directly and probe the
752
+ returned value, mirroring `useSafeFieldLabel`.
753
+
754
+ Adds `object-ui/no-try-catch-around-hook` (error) so a twelfth copy fails CI.
755
+ It only matches `use*` names, accepts member calls solely on `React` (so
756
+ `vi.useRealTimers()` is not a hook), and resets its try-depth inside nested
757
+ functions (so `renderHook(() => useThing())` inside a `try` is fine) — both
758
+ false positives were real code in this repo and are pinned in the rule's tests.
759
+
760
+ `eslint-rules/**/*.test.js` matched no vitest project glob, so the local
761
+ plugin's specs had never run in CI. They are now included; all three pass.
762
+
763
+ `ObjectTimeline`'s test mock of `@object-ui/react` omitted `useObjectLabel` —
764
+ the removed `try`/`catch` had been silently absorbing that gap. The mock is now
765
+ complete.
766
+
767
+ - 8aae006: fix(views): the five per-view-type configs speak the spec vocabulary (#2231 phase 3)
768
+
769
+ `kanban`/`calendar`/`gantt`/`gallery`/`timeline` on `ListViewSchema` were the last
770
+ hand-written forks left after #2882 — and the fork was not cosmetic: objectui named
771
+ the same concepts differently from `@objectstack/spec/ui`, and several read-sites
772
+ only understood one of the two dialects. Two of those gaps were live bugs.
773
+
774
+ **Kanban lanes ignored the spec key.** `ListView` gated the Kanban tab on
775
+ `groupByField || groupField` but rendered lanes off `groupField` alone. A config
776
+ authored with the spec key — which is exactly what the product's own
777
+ `CreateViewDialog` emits — offered the tab and then grouped by whatever
778
+ `detectStatusField()` guessed. The spec's `columns` (the fields shown on each card)
779
+ was also spread onto the board verbatim, where `columns` means _lanes_, so
780
+ `ObjectKanban` built lanes with `undefined` id and title. `columns` now maps to
781
+ `cardFields` and the vocabulary keys are stripped from the passthrough.
782
+
783
+ **Timeline lost every spec key in app-shell.** `ObjectView`'s `timeline` branch was
784
+ a three-key whitelist while its `gallery`/`gantt` siblings had already been fixed to
785
+ spread-first, so a stored `timeline: { startDateField, endDateField, groupByField,
786
+ colorField, scale }` arrived with only `titleField` and an axis pinned to the
787
+ `'due_date'` fallback.
788
+
789
+ Also: `plugin-view`'s `ObjectView` now reads `gallery.coverField` and
790
+ `timeline.startDateField` (it only understood the legacy aliases), and the dead
791
+ `gallery.subtitleField` is removed — three producers computed it and `ObjectGallery`
792
+ never read it.
793
+
794
+ The schema side now derives from the spec configs (`.partial()`, since the product
795
+ authors partial configs and spec marks `columns`/`titleField`/`startDateField`
796
+ required). `gantt` needed no local schema at all. The pre-#2231 names
797
+ (`groupField`, `cardFields`, `imageField`, `dateField`) remain accepted as deprecated
798
+ aliases so stored views keep validating; the spec key wins wherever both appear.
799
+ `calendar.defaultView` stays local — it has no spec counterpart.
800
+
801
+ - Updated dependencies [7b21891]
802
+ - Updated dependencies [0b3be01]
803
+ - Updated dependencies [3c4d935]
804
+ - Updated dependencies [4b1ed7d]
805
+ - Updated dependencies [4b60d2d]
806
+ - Updated dependencies [952b978]
807
+ - Updated dependencies [de5e40c]
808
+ - Updated dependencies [1a03af6]
809
+ - Updated dependencies [3e886eb]
810
+ - Updated dependencies [cfc675e]
811
+ - Updated dependencies [20df08c]
812
+ - Updated dependencies [1767124]
813
+ - Updated dependencies [8ecf5a6]
814
+ - Updated dependencies [af705b9]
815
+ - Updated dependencies [0502a7c]
816
+ - Updated dependencies [7b35e4b]
817
+ - Updated dependencies [8fb1295]
818
+ - Updated dependencies [e16ed2d]
819
+ - Updated dependencies [c6fd752]
820
+ - Updated dependencies [f9bbddb]
821
+ - Updated dependencies [dfd3705]
822
+ - Updated dependencies [c77108c]
823
+ - Updated dependencies [2735de6]
824
+ - Updated dependencies [697cda4]
825
+ - Updated dependencies [c19ac11]
826
+ - Updated dependencies [6dee2cb]
827
+ - Updated dependencies [e05f052]
828
+ - Updated dependencies [0502a7c]
829
+ - Updated dependencies [faad45e]
830
+ - Updated dependencies [09c6a17]
831
+ - Updated dependencies [c7cff19]
832
+ - Updated dependencies [ba73a02]
833
+ - Updated dependencies [cd09a7b]
834
+ - Updated dependencies [f1abf0e]
835
+ - Updated dependencies [f05b84e]
836
+ - Updated dependencies [9b4b952]
837
+ - Updated dependencies [341bfb5]
838
+ - Updated dependencies [2f947e4]
839
+ - Updated dependencies [7d46648]
840
+ - Updated dependencies [9b53d72]
841
+ - Updated dependencies [bb4aa25]
842
+ - Updated dependencies [75f1cdf]
843
+ - Updated dependencies [662bdf9]
844
+ - Updated dependencies [059a052]
845
+ - Updated dependencies [53642d4]
846
+ - Updated dependencies [8aae006]
847
+ - Updated dependencies [c6cfdf1]
848
+ - Updated dependencies [d147a13]
849
+ - Updated dependencies [c6aaed8]
850
+ - Updated dependencies [263f885]
851
+ - Updated dependencies [dc334da]
852
+ - @object-ui/components@17.0.0
853
+ - @object-ui/i18n@17.0.0
854
+ - @object-ui/fields@17.0.0
855
+ - @object-ui/react@17.0.0
856
+ - @object-ui/types@17.0.0
857
+ - @object-ui/core@17.0.0
858
+ - @object-ui/permissions@17.0.0
859
+ - @object-ui/mobile@17.0.0
860
+
3
861
  ## 16.1.0
4
862
 
5
863
  ### Patch Changes