Skip to content

Commit 6e3e546

Browse files
feat(spec)!: retire the list view's own tabs key; named presets are listViews entries (#20357)
Part of #20301 Clause-②: no (narrowing) Stage 1 of a staged card: the list view's own `tabs` key only. #20301 remains open for its second key, the view container's body `name`, which waits until cloud's writer stops sending it (triage note 2). This PR does not touch the container's `name`. ## What `ListViewSchema.tabs` (and the same key on `ObjectListViewSchema`, a view item record's list `config`, and the flattened list overlay at `PUT /api/v1/meta/view`) is retired under ADR-0049 enforce-or-remove. The triage verdict was RETIRE under the maintainer's family criterion: named-view switching is mainstream and is already delivered here by `listViews`, rendered by the saved-view switcher (`ViewTabBar`). - **Tombstone.** `tabs` becomes a `retiredKey()` tombstone inside `ListViewShapeSchema` only, beside the `pageName` tombstone on the same strict shape. Authoring it is a `tsc` error (input type `never`) and a parse error that carries the prescription: delete the key, and move each tab to a named `listViews` entry (tab `name` becomes the entry key, `label` the entry `label`, its `filter` rules join the view's own `filter`, copy `columns`). - **ADR-0087.** D2 conversion `view-list-tabs-removed` (protocol 18, retired from the load path) strips the key from every list payload in `stack.views[]`, in all three persisted spellings. **D2 chosen**: the delete is lossless in pixels because nothing ever read or drew the key, so a stored `view` row replays without it at every rehydration seam and re-saves through the strict write door instead of being refused there over a key that never had an effect. One D3 entry, `list-view-tabs-retired`, per ruling B on #17152, with no tracker number in any author-shown field. `RETIRED_KEYS_BY_MAJOR[18]` gains `ui/ListView:tabs` and `ui/ObjectListView:tabs`. Declared boundary (same as the three sibling list-view conversions): an object's own `listViews` is reached by no conversion, so such an object is refused at its own door. - **Ledger.** The row `view.json` `/props/list/children/tabs` stays `dead` (tombstone discipline), re-verified 2026-09-27, with a REMOVED note. The stale `view/list.tabs` row leaves `undrilled-containers.baseline.json` (the tombstone is no longer a container, which `check:liveness` reported). - **Kit.** The metadata form's `tabs` repeater leaves with the key; the extracted form-label bundles are regenerated (pure deletion, 4 files). Reference docs, `authorable-surface/ui.json` (two rows become `[RETIRED]`) and the migration registry are regenerated by their generators. The published `objectstack-ui` skill's list-view rules stop teaching the key. `layout-dsl.mdx` stops pointing `ViewTab` at list views. Changeset: `@objectstack/spec` minor, BREAKING, FROM to TO table. - **Tests that pinned the retired acceptance** were flipped or re-pointed: `view.test.ts` drops "should accept list view with tabs" and the key from the full-parity fixture; two wire-door tests move their tab carrier to the surviving `userFilters.tabs`. `ViewTabSchema` is **not** retired: the page-only `userFilters.tabs` preset bar reuses it and renders. Pinned as a boundary. ## Measured first (premise holds) - What is true, stated first: **a list view's own `tabs` has no reader**, and objectui's `TabBar`, the one component that would draw it, has zero production mounts. `userFilters.tabs` is a different key with the same element type (`z.array(ViewTabSchema)`): objectui's `UserFilters` passes it to `TabFilters`, which renders it in production as a page list's preset bar, and it stays. - objectui at the pin `f8a9d0fb` (`.objectui-sha`): `git grep -E` for a `TabBar` JSX mount = 10 hits in 2 files, both test files (`components/__tests__/TabBar.i18nLabel.test.tsx`, `__tests__/icon-seam-fallbacks-5935.test.tsx`); every `TabBar` import is a test's. **Lit control**, same instrument and corpus: a `ViewTabBar` JSX mount in production source = 1 file, `packages/app-shell/src/views/ObjectView.tsx` (`views={viewTabItems}`, built from `objectDef.listViews`). No production `.tabs` read of a list view anywhere in `packages/*/src` at the pin. objectui's type mirror (`types/src/objectql.ts`, `tabs?: ListViewSchema['tabs']`) and its zod twin import the spec field by reference, so the tombstone flows in on the next pin bump, the `pageName` precedent; no export is removed here, so the Console Pin Gate build is unaffected. - This repo at base `4e0f72e8d`: zero list-view `tabs` authorings in `examples/` or in platform sources (the 2 `tabs:` hits in `examples/` are a page `userFilters` preset bar and a record-page slot); **control**: `listViews:` authored in 9 example files. The one published skill example that taught the key (`skills/objectstack-ui/rules/list-views.md`) is corrected here. No platform writer sends it. - Readers in this repo: two author-time reference walks read the key off RAW input (`packages/lint/src/validate-list-view-field-refs.ts#checkTabs`, `computeViewReferenceDiagnostics` in `packages/metadata-protocol/src/metadata-diagnostics.ts`). Neither draws a tab bar; the ledger already graded them as not delivering the key's effect. Left in place, see Acceptance notes. `packages/lint/src/validate-capability-references.ts:186` (a generic `rec.tabs` walk) does NOT reach a list view: its walk starts at `stack.apps[*]` and descends only an app node's `navigation` / `areas` / `tabs` / `children` / `items`, so it never visits a view. A third reader, found by CI and not by the pre-change census: the derived reference-site index behind `findReferencesToMeta` (the Studio "Used by" panel) walks each type's schema, and `listViews.*.tabs[].view` (a `ViewTab` naming a list view) was the ONLY property under `object` spelled as a `view` reference. With the key retired the walk finds none, so a view's "Used by" scan no longer reads `object` rows. Measured by an ablation of the tombstone over the `object` input schema: 1 view-spelled path now (`listViews.*.navigation.view`, itself retired), 2 with the tombstone removed (plus `listViews.*.tabs[].view`). No such reference is authorable any more; a stored object row still carrying one is refused at its own door (no conversion reaches an object's `listViews`). - hotcrm's inert-key test (#16094) is inherited, NOT MEASURED here (out of this seat's repo scope). Cloud: NOT MEASURED here; the card's census at cloud `96eb092` names cloud writers for `view.name` only. ## Ablation (the tombstone is what shuts every door) Run on committed HEAD `74f8ab9727` with `node scripts/ablation-replace.mjs` (wrap mode, restore armed on EXIT/INT/TERM) against `packages/spec/src/ui/view.zod.ts`: - mutation landed on disk: anchor ` tabs: retiredKey(` x1 to x0, replacement (`tabs` back to a plain `ViewTab` array, the tombstone parked on a dummy key) x0 to x1, blob `b64e9996f3fc` to `90f0a141e429`; - `vitest run src/ui/view-list-tabs-retirement.test.ts`: **11 failed, 18 passed (29)**. Red: the refusal at all seven doors, the empty-array pin, the prescription pin, the parse-channel half of the tsc pin, and the object-door boundary. Still green, as expected: the D2 conversion, registration, form, `userFilters.tabs` boundary and absence pins, which do not depend on the tombstone. Direction observed: turned red; - restore proven by bytes, not exit code: blob after restore `b64e9996f3fc` equals the HEAD blob, `git diff HEAD` empty, tombstone anchor count 1, mutant count 0. The tsc door is proven by the positive run: `check:test-typecheck` is green with an `@ts-expect-error` on the `tabs` line of a `defineView` call, and an unneeded directive is itself a TS2578 in that same program. It was not separately ablated under tsc. ## Verification **Rework round 1, head `606046e96a`.** The at-tier review failed on the truth of two sentences; the retirement itself stood. (1) "The one component that reads a `ViewTab[]` has zero production mounts" was false, because `userFilters.tabs` is a `ViewTab[]` that `TabFilters` renders. It now reads: a list view's own `tabs` has no reader, and `TabBar`, the one component that would draw it, has zero production mounts; `userFilters.tabs` is read, rendered, and stays. Corrected in the changeset, the tombstone docblock in `view.zod.ts`, both retired-key entries, the conversion docblock, the step-18 rationale, the ledger note, the pin's header, and this body. (2) "examples, skills or platform sources" now reads "examples or platform sources", plus the note that the one published skill example that taught it is corrected here. `main` merged at `d0003a10c7` with `scripts/pm/os-regen-merge.sh`: the hand-written registry tails conflicted with #20251 and both sides were kept, main's first. That also repaired a join from the previous merge that had dropped a period and space before this PR's rationale sentence. The object reference page was regenerated at `8845cb564e`, and the prose corrections landed at `c9f81fa132`. `main` moved again (the RLS policy `tags` retirement) and was merged at `606046e96a`, again keeping both registry tails, main's first; the migration registry regenerated byte-identically. Checks at `c9f81fa132`: spec build + `check:generated` all 15 up to date; `check:liveness`, `check:adr-0087-registration`, `check-changeset-no-major`, `check-empty-changeset`, `check:doc-authoring`, `check:nul-bytes`, `check:issue-citations`, spec docblock anchors, comment-mask adoption and `check:cross-package-test-inputs` all exit 0; the changeset's `Clause-②: no (narrowing)` line reads back as declared. Targeted spec tests at `c9f81fa132`: 5 files / 863 tests passed (`view-list-tabs-retirement`, `conversions`, `migrations`, `view`, `retired-key-migrate-sentence`). At `606046e96a`: the source-reading spec gates (`check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`, `check:liveness`, `check:authorable-surface`, `check:docs`), `check-comment-mask-corpus` and `check:query-options-erasure` all exit 0. The shared verify lock was not acquired for a local post-merge spec rebuild. CI on `606046e96a`: 35 check runs, 33 success and 2 skipped, including TypeScript Type Check (the generated-artifact gates), Test Core 6/6 and Lint & Repo Gates. Derived gates at `606046e96a`: 122, 114 run green, 8 NOT MEASURED. Five of those lost their build prerequisites when a local turbo rebuild was OOM-killed on the shared box: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift` and `check:lean-entry-closure`, all green at `a3f8054dcc` and covered by CI. The other three are `check:i18n`, `check:dual-build-cjs-loads` and `check:type-check-debt`. **CI repair round, head `a3f8054dcc`** (main merged again at `bfa765ca08`, reference pages regenerated at `c13e610ab5`). Four consumer pins of the retired key, in packages the first round did not run, moved to the post-retirement truth, none weakened: `repeater-row-properties.test.ts` now asserts `view:tabs` ABSENT beside the two surviving view repeaters; `object-lifecycle-panel-echo-decisions.test.ts` positive control 608 to 598 (the repeater's label plus its nine row labels), with the reason in the pin's comment; `protocol.graft-normalized-operators.test.ts` keeps its view-filter leg and moves the nested `ViewTab.filter` leg to the surviving `userFilters.tabs` carrier; `protocol.read-seam-empty-accumulator.test.ts` reads the consulted source types from the derived index and pins `object` absent (see Measured first). Full suites at `a3f8054dcc`: `@objectstack/platform-objects` 55 files / 911 tests passed; `@objectstack/metadata-protocol` 189 files passed, 3 skipped (pre-existing) / 2736 tests passed; `@objectstack/lint` 112 files / 4640 tests passed; `@objectstack/objectql` `metadata-diagnostics.test.ts` 10 passed. Derived gates: 122, 119 run green, 3 NOT MEASURED (`check:i18n`: its CLI prerequisite build never acquired the shared lock, green at `baa383008b` on the same bundles; `check:dual-build-cjs-loads`; `check:type-check-debt`). First round, at merged HEAD `baa383008b` unless stated. - `pnpm --filter @objectstack/spec build` then `check:generated`: **All 15 generated artifacts are up to date** (migration registry, spec-changes, upgrade guide, authorable surface, api-surface, docs, liveness counts, test-typecheck and the rest). - `pnpm --filter @objectstack/spec typecheck`: exit 0 (tsc, scripts typecheck, and test typecheck: 53 files / 255 errors / 142 pinned signatures held, unchanged). - `pnpm --filter @objectstack/spec test` (local project): **554 files passed, 16426 tests passed**, 1 todo. - Repo project: full `test:repo` at `74f8ab9727`: **34 files, 633 tests passed**. After merging `main` again (the flattened-overlay `owner` / `hidden` retirement landed and conflicted only in the two hand-written registry tails; both sides kept), targeted rerun of the two repo-project retirement pins at `baa383008b`: 2 files, 47 tests passed. Declared narrowing: the other 32 repo-project files were not rerun after that merge; CI runs them. - i18n: `node scripts/check-i18n-bundles.mjs --write` regenerated the four `metadata-forms` bundles; `pnpm check:i18n` exit 0. - Derived gate families (`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, reconciled with `--ran`): **121 derived, 119 run, 2 NOT MEASURED, 0 unrun**. Named in the dispatch and green: `check:liveness`, `check:migration-registry`, `check:generated`, `check:spec-changes`. - NOT MEASURED: `check:dual-build-cjs-loads` (exit 3, needs a full workspace build), `check:type-check-debt` (repo-wide tsc re-measure of every package exceeded the foreground cap on a shared box). - `check:dts-closure` exit 1 on tree state, not on this diff: five packages this PR does not touch (cloud-connection, organizations, plugin-approvals, plugin-dev, verify) hold JS-only `dist/` in this worktree after a shared turbo cache replay during an unrelated closure build; the same gate was exit 0 at `74f8ab9727` before that build. CI builds fresh. - Lint, narrowed and proven: eslint `--no-inline-config --format json` over the 15 changed JS/TS files: 15 files, 0 errors, 0 warnings, none ignored. Population is `pnpm lint` (`eslint . --no-inline-config`, `eslint.config.mjs`); invariance: that config enables no type-aware linting (no `parserOptions.project`, stated in `eslint.config.mjs`), so this diff cannot move any untouched file's verdict. ## Governed surface: Tier H `skills/objectstack-ui/rules/list-views.md` is under `skills/**`, so this PR is **Tier H**: it lands only by the maintainer's hand or an authorized approval. No generated file under `skills/**` changed. Readings for the `skills/**` edit (lines): the edited file 309 to 302 (net -7, pure deletion: the list-level `tabs` example and a rule that described a tab bar that never rendered, replaced by a 3-line `listViews` pointer). Whole pack, all 10 `skills/**/SKILL.md`: 4402 to 4402 (no SKILL.md edited). ## Acceptance notes Out of this card's file surface, noted and not filed (dead code class; carrier named): 1. The two author-time walks above still read a list view's `tabs` off raw input, and their test fixtures still author it (`packages/lint/src/validate-list-view-field-refs.test.ts`, `packages/objectql/src/metadata-diagnostics.test.ts`, plus the CLI's negative i18n pin `packages/cli/test/i18n-tab-coverage.test.ts`). After this PR the parse refuses the key first, so those branches are unreachable for valid input. The tree-scoped absence pin declares these three files as self-expiring residue: each is asserted to still hold an offender, so the day a follow-up deletes the walks, the pin forces the entry out. Carrier: none named. 2. `packages/cli/src/utils/i18n-extract.ts` and `packages/lint/src/validate-translation-references.ts` carry prose that says the list view's own `tabs` "has no reader"; still true, now for a stronger reason. Carrier: none named. 3. objectui at its next pin bump: `types/src/objectql.ts` keeps `tabs?: ListViewSchema['tabs']` (becomes `undefined`-typed) and its parity tests may want the row updated. Carrier: the objectui pin-bump PR. ## 维护者速读(草稿) **改了什么** 列表视图上的 `tabs` 字段被正式退役。以后在列表视图里写 `tabs`,类型检查和保存都会直接报错,错误信息会告诉作者改用 `listViews`(每个命名视图就是切换条上的一个页签)。存量数据里残留的这个字段,升级时会被自动删掉。 **为什么改** 这个字段一直能写、能存,但没有任何代码读它,界面上也从来没有画出过任何东西:本来要画它的那个页签组件在正式代码里一次都没被用上;真正显示在记录列表上方的页签条,是按 `listViews` 逐个列出的已存视图。页面筛选栏里的 `userFilters.tabs` 是另一个字段,照常显示,不受影响。同一个能力两种写法,其中一种完全无效,AI 和作者都容易写错。按“主流平台已有、本平台已交付 ⇒ 只保留一种写法”的判据退役。 **风险与代价(含回滚)** 属于破坏性变更:外部仓库里若有人写了这个字段,升级后会被拒绝(报错里带修改方法)。本仓示例与平台代码实测零使用,唯一教过这种写法的已发布技能示例已在本 PR 中改正;外部使用面未测量。存量数据自动清理,不影响任何已有画面。回滚:revert 本 PR 即可恢复该字段。 **席位意见** **你要做的** 本 PR 触及已发布技能目录(`skills/**`),属于 Tier H,需要你本人批准后才能合并。 --- _Generated by [Claude Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0fcb101 commit 6e3e546

29 files changed

Lines changed: 1039 additions & 243 deletions
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: retire the list view's own `tabs` key — parsed, stored, and drawn by nothing; named presets are `listViews` entries
6+
7+
**BREAKING** — `tabs` is removed from the list view (`ListViewSchema`,
8+
`ObjectListViewSchema` — a `defineView` container's `list` / `listViews`, an
9+
object's `listViews` — a view item record's list `config`, and the flattened
10+
list overlay the `PUT /api/v1/meta/view` door accepts). ADR-0049
11+
enforce-or-remove; triage verdict RETIRE, on the rule that a capability the
12+
mainstream has and this platform already delivers keeps ONE spelling.
13+
14+
The key parsed at every list-view door and was stored, and no renderer ever
15+
drew it. Measured before removal, each reading beside a lit control: a list
16+
view's own `tabs` has no reader, and objectui's `TabBar` — the one component
17+
that would draw it — has zero production mounts at the objectui commit this
18+
repo pins (every occurrence is in its own two test files), while the saved-view
19+
switcher (`ViewTabBar`) mounts in the object view and is fed from the object's
20+
`listViews`. That switcher IS the tab strip above an object's records: one tab
21+
per named list view. `userFilters.tabs` is a different key with the same
22+
element type: it is read and rendered as a page list's preset bar, and it
23+
stays. Zero list views in this repo's examples or platform sources authored the
24+
key; the one published skill example that taught it is corrected here.
25+
26+
### FROM → TO
27+
28+
| removed | what to write instead |
29+
| --- | --- |
30+
| a list view's `tabs: [{ name, label, filter, … }]` | one named list view per tab, under the object's `listViews`: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too). A tab whose `view` already named a list view needs nothing more. |
31+
| the tab keys `icon`, `order`, `pinned`, `isDefault`, `visible` | nothing — none of them ever had an effect. |
32+
33+
**The one-line fix: delete `tabs:` from every list view, and add a `listViews`
34+
entry for each tab you want users to switch to.** `os migrate meta --from 17`
35+
lists the mechanical edits for existing sources; apply them by hand.
36+
37+
```ts
38+
// before — parsed clean, drew no tab bar
39+
defineView({
40+
object: 'crm_ticket',
41+
list: {
42+
type: 'grid', columns: ['subject', 'status'],
43+
tabs: [{ name: 'open', label: 'Open', filter: [{ field: 'status', operator: 'equals', value: 'open' }] }],
44+
},
45+
});
46+
// after — the switcher above the records shows "Open" beside the default view
47+
defineView({
48+
object: 'crm_ticket',
49+
list: { type: 'grid', columns: ['subject', 'status'] },
50+
listViews: {
51+
open: {
52+
type: 'grid', label: 'Open', columns: ['subject', 'status'],
53+
filter: [{ field: 'status', operator: 'equals', value: 'open' }],
54+
},
55+
},
56+
});
57+
```
58+
59+
⛔ **Untouched: the page-only preset bar.** `userFilters: { element: 'tabs',
60+
tabs: [...] }` on a page list is a different key, it renders, and
61+
`ViewTabSchema` stays for it.
62+
63+
### The retirement kit
64+
65+
- **A `retiredKey()` tombstone on the list-view shape**, beside the `pageName`
66+
tombstone on the same strict shape. Every door built from it refuses: `tsc`
67+
types the key `never`, and the parse raises the prescription (which names the
68+
move to `listViews`) instead of a bare unknown-key report.
69+
- **D2 conversion `view-list-tabs-removed`** (protocol 18, retired from the load
70+
path): strips `tabs` from every list payload in `stack.views[]`, in all three
71+
persisted spellings, as a lossless delete — nothing ever drew the tabs — so a
72+
stored `view` row replays clean through the rehydration seam. An object's own
73+
`listViews` is reached by no conversion, so such an object is refused at its
74+
door until edited by hand.
75+
- **D3 entry `list-view-tabs-retired`** beside it, carrying the part no
76+
conversion can decide: which tabs deserve a `listViews` entry.
77+
- **`RETIRED_KEYS_BY_MAJOR[18]`**: `ui/ListView:tabs`, `ui/ObjectListView:tabs`;
78+
both `authorable-surface/ui.json` rows become `[RETIRED]`.
79+
- **The metadata form's `tabs` repeater** leaves with the key, and the
80+
extracted form-label bundles are regenerated.
81+
- **The liveness row stays `dead`**, re-verified, with a REMOVED note — the
82+
tombstone keeps the key in the walked shape.
83+
- **The published `objectstack-ui` skill** no longer teaches the key: its
84+
list-view rules example and the "tabs win over dropdowns" rule (which
85+
described a tab bar that never rendered) are replaced by the `listViews`
86+
pointer.
87+
- **Pins** (`ui/view-list-tabs-retirement.test.ts`): the refusal, its issue
88+
code, path and prescription at seven doors, each with a lit control; the tsc
89+
channel; the `userFilters.tabs` boundary; the conversion's reach, boundary and
90+
idempotence; the D2/D3 registration; and a tree-scoped absence walk over the
91+
declared radius.
92+
- **No deprecation window**, per the project's startup-stage posture.
93+
94+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec`
95+
is published, so this is breaking for consumers no telemetry was consulted for.
96+
97+
Clause-②: no (narrowing)
98+
99+
<!-- adr-0087: registered view-list-tabs-removed, list-view-tabs-retired -->

‎content/docs/protocol/objectui/layout-dsl.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -563,10 +563,10 @@ is a **parse failure** — a loud rejection, not a silent no-op.
563563
</Callout>
564564

565565
Looking for tabs that carry their own `name`, `icon`, `filter`, `order`,
566-
`pinned` and `isDefault`? That surface exists, but it belongs to **list** views,
567-
not forms: `ui/ViewTab` declares exactly nine keys (`filter`, `icon`,
568-
`isDefault`, `label`, `name`, `order`, `pinned`, `view`, `visible`), and each
569-
tab points at a named list view. See
566+
`pinned` and `isDefault`? That surface belongs to a **page list's** preset bar
567+
(`userFilters.tabs`), not to forms: `ui/ViewTab` declares exactly nine keys (`filter`,
568+
`icon`, `isDefault`, `label`, `name`, `order`, `pinned`, `view`, `visible`). A list
569+
view has no `tabs` key — an object's tab strip lists its named `listViews`. See
570570
[View Reference → ViewTab](/docs/references/ui/view#viewtab).
571571

572572
## Responsive Layout Modifiers

‎content/docs/references/api/protocol.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,7 +1699,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
16991699
| **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'json'>[] \| { formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export configuration for the list toolbar export menu: `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`. A bare format array is the legacy spelling and lifts to `{ formats: [...] }` at parse. |
17001700
| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar |
17011701
| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration |
1702-
| **tabs** | `{ name: string; label?: string \| Record<string, string>; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface |
1702+
| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
17031703
| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration |
17041704
| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list |
17051705
| **allowPrinting** | `boolean` | optional | Allow users to print the view |
@@ -1784,7 +1784,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
17841784
| **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'json'>[] \| { formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export configuration for the list toolbar export menu: `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`. A bare format array is the legacy spelling and lifts to `{ formats: [...] }` at parse. |
17851785
| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar |
17861786
| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration |
1787-
| **tabs** | `{ name: string; label?: string \| Record<string, string>; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface |
1787+
| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
17881788
| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration |
17891789
| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list |
17901790
| **allowPrinting** | `boolean` | optional | Allow users to print the view |

‎content/docs/references/data/object.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ const result = ApiMethod.parse(data);
398398
| **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'json'>[] \| { formats?: Enum<'csv' \| 'xlsx' \| 'json'>[]; maxRecords?: integer; includeHeaders?: boolean; fileNamePrefix?: string; … }` | optional | Export configuration for the list toolbar export menu: `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`. A bare format array is the legacy spelling and lifts to `{ formats: [...] }` at parse. |
399399
| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar |
400400
| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration |
401-
| **tabs** | `{ name: string; label?: string \| Record<string, string>; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface |
401+
| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
402402
| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration |
403403
| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list |
404404
| **allowPrinting** | `boolean` | optional | Allow users to print the view |

0 commit comments

Comments
 (0)