From f9b39c3d589cc46fcee40d24cdc4a01e18010f89 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 09:28:51 +0000 Subject: [PATCH 1/6] fix(spec): offer the view form's pagination to every view type `pagination.pageSize` is the one row bound a view carries, and for a kanban, gallery or timeline view it is the only one. The Studio view form offered `pagination` only inside the grid-only `table_options` section, so an author of any other view type could not reach it. `pagination` moves to its own section with no `visibleWhen`; the grid-only fields stay in `table_options`. A pin derives the type list from the list-view `type` enum and asserts both halves. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .../spec/src/ui/view-form-pagination.test.ts | 152 ++++++++++++++++++ packages/spec/src/ui/view.form.ts | 14 +- 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 packages/spec/src/ui/view-form-pagination.test.ts diff --git a/packages/spec/src/ui/view-form-pagination.test.ts b/packages/spec/src/ui/view-form-pagination.test.ts new file mode 100644 index 00000000000..4563b1f543d --- /dev/null +++ b/packages/spec/src/ui/view-form-pagination.test.ts @@ -0,0 +1,152 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The Studio view form offers `pagination` to EVERY view type, and keeps the + * grid-only display options grid-only. + * + * ## What is being pinned + * + * Maintainer ruling D on #19228: a view carries one row bound, + * `pagination.pageSize`. For a kanban, gallery or timeline view it is the only + * one. The form used to offer `pagination` only inside `table_options` + * (`visibleWhen: "data.type == 'grid' || data.type == null"`), so an author + * editing any other view type could not see or set that bound short of editing + * the metadata by hand. Two halves, both asserted: + * + * - **offer**: for every value of the list-view `type` enum — and for a view + * with no `type` yet — some section visible to that type offers `pagination`; + * the offer is backed by the door, because every type parses a `pagination` + * block clean and keeps it; + * - **no leak**: the grid-only fields stay hidden from every non-grid type. + * + * ## The kind list is read from the enum, never written here + * + * A literal list would stay green through a new view type the form hides + * `pagination` from. The list is the enum on {@link ListViewSchema} — the + * list-view shape this form lays out, and the one `ViewSchema`'s `list` / + * `listViews` entries are derived from — with a floor naming the four types + * the ruling is about, so an empty derivation cannot pass. + * + * ## How visibility is read + * + * A section's `visibleWhen` is a CEL predicate over the edited record as + * `data`. `packages/spec` carries no evaluator and must not grow one (no + * runtime logic in spec), so {@link visibleFor} reads the ONE grammar this form + * uses — a disjunction of `data.type == ''` / `data.type == null` terms — + * and THROWS on anything else, naming the predicate. A predicate this pin cannot + * read fails it loudly instead of being guessed at. + */ + +import { describe, it, expect } from 'vitest'; + +import { viewForm } from './view.form'; +import { ListViewSchema } from './view.zod'; + +type Predicate = string | { dialect?: string; source?: string } | undefined; +type Entry = string | { field?: string; visibleWhen?: Predicate }; +type Section = { name?: string; visibleWhen?: Predicate; fields?: Entry[] }; + +const SECTIONS = (viewForm.sections ?? []) as Section[]; + +/** The list-view `type` enum, read at runtime. */ +const VIEW_TYPES: readonly string[] = ( + ListViewSchema.shape.type as unknown as { unwrap(): { options: readonly string[] } } +).unwrap().options; + +/** The view types ruling D names — a floor under the derivation, not the list. */ +const RULING_D_TYPES = ['grid', 'kanban', 'gallery', 'timeline'] as const; + +/** + * The fields that are grid-only by design. Named here rather than read from + * `table_options`, so moving one out of that section is a visible failure, not a + * silently re-derived list. + */ +const GRID_ONLY_FIELDS = ['resizable', 'compactToolbar', 'rowHeight', 'selection'] as const; + +const TERM = /^data\.type\s*==\s*(?:'([a-z_]+)'|(null))$/; + +/** + * Is a predicate true for a record whose `type` is `type` (`undefined` = not + * set yet)? An absent predicate is always visible. + */ +function visibleFor(predicate: Predicate, type: string | undefined, where: string): boolean { + if (predicate === undefined) return true; + const source = typeof predicate === 'string' ? predicate : predicate.source; + if (typeof predicate !== 'string' && predicate.dialect !== undefined && predicate.dialect !== 'cel') { + throw new Error(`${where}: this pin reads CEL predicates only, got dialect ${JSON.stringify(predicate.dialect)}`); + } + if (typeof source !== 'string') throw new Error(`${where}: predicate has no source: ${JSON.stringify(predicate)}`); + return source.split('||').map((t) => t.trim()).some((term) => { + const m = TERM.exec(term); + if (!m) { + throw new Error( + `${where}: this pin cannot read the predicate term ${JSON.stringify(term)} in ${JSON.stringify(source)} — ` + + 'extend visibleFor() to read it; never guess a visibility', + ); + } + return m[2] === 'null' ? type === undefined : type === m[1]; + }); +} + +const fieldName = (e: Entry): string | undefined => (typeof e === 'string' ? e : e.field); + +/** The sections (by name) in which `field` is offered AND visible for `type`. */ +function offeredTo(field: string, type: string | undefined): string[] { + const hits: string[] = []; + for (const section of SECTIONS) { + const where = `section ${JSON.stringify(section.name)}`; + if (!visibleFor(section.visibleWhen, type, where)) continue; + for (const entry of section.fields ?? []) { + if (fieldName(entry) !== field) continue; + const own = typeof entry === 'string' ? undefined : entry.visibleWhen; + if (visibleFor(own, type, `${where} field ${JSON.stringify(field)}`)) hits.push(section.name ?? '(unnamed)'); + } + } + return hits; +} + +describe('view form — the kind list is the enum', () => { + it('derives a non-empty type list that contains every type ruling D names', () => { + expect(VIEW_TYPES.length).toBeGreaterThanOrEqual(RULING_D_TYPES.length); + for (const t of RULING_D_TYPES) expect(VIEW_TYPES, `the enum lost '${t}'`).toContain(t); + }); +}); + +describe('view form — `pagination` is offered to every view type', () => { + it('is offered exactly once in the whole form', () => { + const entries = SECTIONS.flatMap((s) => (s.fields ?? []).filter((e) => fieldName(e) === 'pagination')); + expect(entries).toHaveLength(1); + }); + + it.each(VIEW_TYPES.map((t) => [t]))("is visible for type '%s'", (type) => { + expect(offeredTo('pagination', type), `a '${type}' view cannot reach its row bound in the form`).toHaveLength(1); + }); + + it('is visible for a view whose type is not set yet', () => { + expect(offeredTo('pagination', undefined)).toHaveLength(1); + }); + + it.each(VIEW_TYPES.map((t) => [t]))("is backed by the door: type '%s' parses a pagination block and keeps it", (type) => { + const r = ListViewSchema.safeParse({ type, columns: ['name'], pagination: { pageSize: 50 } }); + expect(r.success, JSON.stringify(r.error?.issues ?? '')).toBe(true); + expect((r.data as { pagination?: unknown }).pagination).toEqual({ pageSize: 50 }); + }); +}); + +describe('view form — grid-only fields stay grid-only', () => { + it.each(GRID_ONLY_FIELDS.map((f) => [f]))("'%s' is visible for grid and for an unset type", (field) => { + expect(offeredTo(field, 'grid')).toHaveLength(1); + expect(offeredTo(field, undefined)).toHaveLength(1); + }); + + const nonGrid = VIEW_TYPES.filter((t) => t !== 'grid'); + it.each(nonGrid.flatMap((t) => GRID_ONLY_FIELDS.map((f) => [f, t])))("'%s' is hidden from type '%s'", (field, type) => { + expect(offeredTo(field, type)).toEqual([]); + }); +}); + +describe('view form — the predicate reader refuses what it cannot read', () => { + it('throws on a term outside its grammar rather than guessing', () => { + expect(() => visibleFor("data.type != 'grid'", 'kanban', 'probe')).toThrow(/cannot read the predicate term/); + }); +}); diff --git a/packages/spec/src/ui/view.form.ts b/packages/spec/src/ui/view.form.ts index bea0c5ab924..bf6c3a7f939 100644 --- a/packages/spec/src/ui/view.form.ts +++ b/packages/spec/src/ui/view.form.ts @@ -58,9 +58,21 @@ export const viewForm = defineForm({ { field: 'compactToolbar', colSpan: 1 }, { field: 'rowHeight', colSpan: 1 }, { field: 'selection', type: 'composite', colSpan: 2 }, - { field: 'pagination', type: 'composite', colSpan: 2 }, ], }, + // `pagination` is NOT grid-only: every view type accepts it, and + // `pagination.pageSize` is the one row bound a view carries (maintainer + // ruling D on #19228) — for a kanban, gallery or timeline view the only + // one. Its own section with no `visibleWhen` puts it in front of every + // type; inside `table_options` a non-grid author could not reach it. + { + name: 'pagination', + label: 'Pagination', + description: 'Page size and page-size options — every view type accepts them, not only grids.', + collapsible: true, + collapsed: true, + fields: [{ field: 'pagination', type: 'composite' }], + }, { name: 'kanban', label: 'Kanban', From 17bb7c1603f59e1fb37ffaef40c1424ae14edf02 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 09:40:15 +0000 Subject: [PATCH 2/6] chore(platform-objects): regenerate metadata-form bundles for the view pagination section Output of `pnpm i18n:extract`, unedited: the view form's new `pagination` section adds its label and description to the four metadata-form bundles, with the three translated locales filled from the source and their provenance recorded in the source-hash tables. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .../src/apps/translations/en.metadata-forms.generated.ts | 4 ++++ .../src/apps/translations/es-ES.metadata-forms.generated.ts | 4 ++++ .../src/apps/translations/es-ES.source-hashes.generated.ts | 2 ++ .../src/apps/translations/ja-JP.metadata-forms.generated.ts | 4 ++++ .../src/apps/translations/ja-JP.source-hashes.generated.ts | 2 ++ .../src/apps/translations/zh-CN.metadata-forms.generated.ts | 4 ++++ .../src/apps/translations/zh-CN.source-hashes.generated.ts | 2 ++ 7 files changed, 22 insertions(+) diff --git a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts index f48f4c61342..f05b4d46c90 100644 --- a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts @@ -730,6 +730,10 @@ export const enMetadataForms: NonNullable = { label: "Table options", description: "Grid-only display options." }, + pagination: { + label: "Pagination", + description: "Page size and page-size options — every view type accepts them, not only grids." + }, kanban: { label: "Kanban", description: "Kanban-specific board configuration." diff --git a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts index e1ff30a45b7..4422d38d33a 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts @@ -730,6 +730,10 @@ export const esESMetadataForms: NonNullable = label: "Opciones de tabla", description: "Opciones de visualización solo de cuadrícula." }, + pagination: { + label: "Pagination", + description: "Page size and page-size options — every view type accepts them, not only grids." + }, kanban: { label: "Tablero Kanban", description: "Configuración de tablero específica de Kanban." diff --git a/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts index 22c23059978..94ea6d2ede2 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts @@ -18,6 +18,8 @@ */ export const esESGeneratedSourceHashes: Readonly> = { + "metadataForms.view.sections.pagination.description": "ccd1ce9181e55cc3", + "metadataForms.view.sections.pagination.label": "309131fbaca258df", "objects.sys_account._actions.link_social.params.provider.options.apple": "cfdc41e15ed6699b", "objects.sys_account._actions.link_social.params.provider.options.discord": "12f931cc062e76ae", "objects.sys_account._actions.link_social.params.provider.options.facebook": "7eea009178f5b807", diff --git a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts index 5da1b245395..10b33befd03 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts @@ -730,6 +730,10 @@ export const jaJPMetadataForms: NonNullable = label: "テーブルオプション", description: "グリッド専用の表示オプション。" }, + pagination: { + label: "Pagination", + description: "Page size and page-size options — every view type accepts them, not only grids." + }, kanban: { label: "カンバン", description: "カンバン専用のボード設定。" diff --git a/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts index 7d47368b7fd..5d260be3885 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts @@ -18,6 +18,8 @@ */ export const jaJPGeneratedSourceHashes: Readonly> = { + "metadataForms.view.sections.pagination.description": "ccd1ce9181e55cc3", + "metadataForms.view.sections.pagination.label": "309131fbaca258df", "objects.sys_account._actions.link_social.params.provider.options.apple": "cfdc41e15ed6699b", "objects.sys_account._actions.link_social.params.provider.options.discord": "12f931cc062e76ae", "objects.sys_account._actions.link_social.params.provider.options.facebook": "7eea009178f5b807", diff --git a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts index c82273d3a8b..8d6de4a1259 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts @@ -730,6 +730,10 @@ export const zhCNMetadataForms: NonNullable = label: "表格选项", description: "仅 Grid 表格的显示选项" }, + pagination: { + label: "Pagination", + description: "Page size and page-size options — every view type accepts them, not only grids." + }, kanban: { label: "看板配置", description: "看板专属配置" diff --git a/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts index c15bc9b9526..26228e1f378 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts @@ -18,6 +18,8 @@ */ export const zhCNGeneratedSourceHashes: Readonly> = { + "metadataForms.view.sections.pagination.description": "ccd1ce9181e55cc3", + "metadataForms.view.sections.pagination.label": "309131fbaca258df", "objects.sys_account._actions.link_social.params.provider.options.apple": "cfdc41e15ed6699b", "objects.sys_account._actions.link_social.params.provider.options.discord": "12f931cc062e76ae", "objects.sys_account._actions.link_social.params.provider.options.facebook": "7eea009178f5b807", From 72185706b2a977c519e87bd417d14b752d1170c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 09:41:24 +0000 Subject: [PATCH 3/6] i18n(platform-objects): translate the view form's pagination section Hand-written zh-CN / ja-JP / es-ES leaves for the new section's label and description (the label reuses each locale's existing `pagination` field label). A re-run of `pnpm i18n:extract` then dropped the three source-hash entries by itself, since those leaves are no longer copies of the source. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .../src/apps/translations/es-ES.metadata-forms.generated.ts | 4 ++-- .../src/apps/translations/es-ES.source-hashes.generated.ts | 2 -- .../src/apps/translations/ja-JP.metadata-forms.generated.ts | 4 ++-- .../src/apps/translations/ja-JP.source-hashes.generated.ts | 2 -- .../src/apps/translations/zh-CN.metadata-forms.generated.ts | 4 ++-- .../src/apps/translations/zh-CN.source-hashes.generated.ts | 2 -- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts index 4422d38d33a..391b1e77ecc 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts @@ -731,8 +731,8 @@ export const esESMetadataForms: NonNullable = description: "Opciones de visualización solo de cuadrícula." }, pagination: { - label: "Pagination", - description: "Page size and page-size options — every view type accepts them, not only grids." + label: "Paginación", + description: "Tamaño de página y opciones de tamaño de página — los aceptan todos los tipos de vista, no solo la cuadrícula." }, kanban: { label: "Tablero Kanban", diff --git a/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts index 94ea6d2ede2..22c23059978 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts @@ -18,8 +18,6 @@ */ export const esESGeneratedSourceHashes: Readonly> = { - "metadataForms.view.sections.pagination.description": "ccd1ce9181e55cc3", - "metadataForms.view.sections.pagination.label": "309131fbaca258df", "objects.sys_account._actions.link_social.params.provider.options.apple": "cfdc41e15ed6699b", "objects.sys_account._actions.link_social.params.provider.options.discord": "12f931cc062e76ae", "objects.sys_account._actions.link_social.params.provider.options.facebook": "7eea009178f5b807", diff --git a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts index 10b33befd03..32fa0f39c11 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts @@ -731,8 +731,8 @@ export const jaJPMetadataForms: NonNullable = description: "グリッド専用の表示オプション。" }, pagination: { - label: "Pagination", - description: "Page size and page-size options — every view type accepts them, not only grids." + label: "ページネーション", + description: "ページサイズとページサイズの選択肢 — グリッドだけでなく、すべてのビュータイプで使用できます。" }, kanban: { label: "カンバン", diff --git a/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts index 5d260be3885..7d47368b7fd 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts @@ -18,8 +18,6 @@ */ export const jaJPGeneratedSourceHashes: Readonly> = { - "metadataForms.view.sections.pagination.description": "ccd1ce9181e55cc3", - "metadataForms.view.sections.pagination.label": "309131fbaca258df", "objects.sys_account._actions.link_social.params.provider.options.apple": "cfdc41e15ed6699b", "objects.sys_account._actions.link_social.params.provider.options.discord": "12f931cc062e76ae", "objects.sys_account._actions.link_social.params.provider.options.facebook": "7eea009178f5b807", diff --git a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts index 8d6de4a1259..789f6f28b69 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts @@ -731,8 +731,8 @@ export const zhCNMetadataForms: NonNullable = description: "仅 Grid 表格的显示选项" }, pagination: { - label: "Pagination", - description: "Page size and page-size options — every view type accepts them, not only grids." + label: "分页", + description: "每页条数与每页条数选项——所有视图类型都接受,不只是 Grid 表格" }, kanban: { label: "看板配置", diff --git a/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts index 26228e1f378..c15bc9b9526 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts @@ -18,8 +18,6 @@ */ export const zhCNGeneratedSourceHashes: Readonly> = { - "metadataForms.view.sections.pagination.description": "ccd1ce9181e55cc3", - "metadataForms.view.sections.pagination.label": "309131fbaca258df", "objects.sys_account._actions.link_social.params.provider.options.apple": "cfdc41e15ed6699b", "objects.sys_account._actions.link_social.params.provider.options.discord": "12f931cc062e76ae", "objects.sys_account._actions.link_social.params.provider.options.facebook": "7eea009178f5b807", From fe2ae4cf09f8c139f933a9cd3446ceeb4b5860d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 09:42:39 +0000 Subject: [PATCH 4/6] chore(changeset): patch spec and platform-objects for the view form pagination section Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .changeset/19814-view-form-pagination-all-kinds.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/19814-view-form-pagination-all-kinds.md diff --git a/.changeset/19814-view-form-pagination-all-kinds.md b/.changeset/19814-view-form-pagination-all-kinds.md new file mode 100644 index 00000000000..b6f7aaedef8 --- /dev/null +++ b/.changeset/19814-view-form-pagination-all-kinds.md @@ -0,0 +1,10 @@ +--- +"@objectstack/spec": patch +"@objectstack/platform-objects": patch +--- + +The Studio view form (`viewForm`, served by `METADATA_FORM_REGISTRY.view`) now offers `pagination` for every view type, not only grids. + +`pagination.pageSize` is the one row bound a view carries; for a kanban, gallery or timeline view it is the only one. The form used to place `pagination` inside the grid-only `Table options` section (shown when `type` is `grid` or unset), so an author editing any other view type could not see or set it without editing the metadata by hand. It now has its own collapsed `Pagination` section with no visibility condition. `Table options` keeps `resizable`, `compactToolbar`, `rowHeight` and `selection`, still for grids only. + +No schema changed: every view type already accepted `pagination`. `@objectstack/platform-objects` ships the new section's label and description in its metadata-form translation bundles (en, zh-CN, ja-JP, es-ES). From 2bfeee4a23c4c149c0c5028f6ec4f356c7ee2ea5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 10:08:01 +0000 Subject: [PATCH 5/6] test(platform-objects): move the translated-label control to 584 The view form's new `pagination` section adds one `.label` leaf, and it is authored in zh-CN, ja-JP and es-ES rather than left an extractor fill, so the per-locale count of translated labels moves by exactly one. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .../object-lifecycle-panel-echo-decisions.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts b/packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts index 8591668a581..193f30de939 100644 --- a/packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts +++ b/packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts @@ -1109,8 +1109,10 @@ describe('#19403 round 10 — the verdicts, on the live bundles', () => { // form row each across ten forms, and authored every one of their labels // in all three locales rather than leaving it an extractor fill — so this // control moves by exactly the number of rows that landed, in every - // locale, which is the reading a per-locale count is for. - expect(translated.length, `${locale} positive control`).toBe(583); + // locale, which is the reading a per-locale count is for. 584 since + // #19814: the view form's new `pagination` section, its label authored + // in all three locales. + expect(translated.length, `${locale} positive control`).toBe(584); } // ⭐ DARK — the blindness, executable. On a synthetic two-locale catalog the // all-three predicate returns 0 while the per-locale one returns 1, so the From 0395fd696d51b326e5ee68b31bd48a398135a18e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 10:52:44 +0000 Subject: [PATCH 6/6] docs(spec): state the pagination row bound as true before and after the per-kind limit retirement The changeset, the form comment and the pin's docblock said `pagination.pageSize` is a view's only row bound. The kanban, gallery and timeline configs still declare a per-kind `limit` until its retirement lands, so all three now say only that `pagination.pageSize` is the row bound every view type carries. Wording only; no form entry, test logic, bundle or translation moves. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .changeset/19814-view-form-pagination-all-kinds.md | 2 +- packages/spec/src/ui/view-form-pagination.test.ts | 6 +++--- packages/spec/src/ui/view.form.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.changeset/19814-view-form-pagination-all-kinds.md b/.changeset/19814-view-form-pagination-all-kinds.md index b6f7aaedef8..b5434a0cc71 100644 --- a/.changeset/19814-view-form-pagination-all-kinds.md +++ b/.changeset/19814-view-form-pagination-all-kinds.md @@ -5,6 +5,6 @@ The Studio view form (`viewForm`, served by `METADATA_FORM_REGISTRY.view`) now offers `pagination` for every view type, not only grids. -`pagination.pageSize` is the one row bound a view carries; for a kanban, gallery or timeline view it is the only one. The form used to place `pagination` inside the grid-only `Table options` section (shown when `type` is `grid` or unset), so an author editing any other view type could not see or set it without editing the metadata by hand. It now has its own collapsed `Pagination` section with no visibility condition. `Table options` keeps `resizable`, `compactToolbar`, `rowHeight` and `selection`, still for grids only. +`pagination.pageSize` is the row bound every view type carries. The form used to place `pagination` inside the grid-only `Table options` section (shown when `type` is `grid` or unset), so an author editing any other view type could not see or set it without editing the metadata by hand. It now has its own collapsed `Pagination` section with no visibility condition. `Table options` keeps `resizable`, `compactToolbar`, `rowHeight` and `selection`, still for grids only. No schema changed: every view type already accepted `pagination`. `@objectstack/platform-objects` ships the new section's label and description in its metadata-form translation bundles (en, zh-CN, ja-JP, es-ES). diff --git a/packages/spec/src/ui/view-form-pagination.test.ts b/packages/spec/src/ui/view-form-pagination.test.ts index 4563b1f543d..808cfbd648e 100644 --- a/packages/spec/src/ui/view-form-pagination.test.ts +++ b/packages/spec/src/ui/view-form-pagination.test.ts @@ -6,9 +6,9 @@ * * ## What is being pinned * - * Maintainer ruling D on #19228: a view carries one row bound, - * `pagination.pageSize`. For a kanban, gallery or timeline view it is the only - * one. The form used to offer `pagination` only inside `table_options` + * `pagination.pageSize` is the row bound every view type carries; maintainer + * ruling D on #19228 makes it the direction for a view's row bound. The form + * used to offer `pagination` only inside `table_options` * (`visibleWhen: "data.type == 'grid' || data.type == null"`), so an author * editing any other view type could not see or set that bound short of editing * the metadata by hand. Two halves, both asserted: diff --git a/packages/spec/src/ui/view.form.ts b/packages/spec/src/ui/view.form.ts index bf6c3a7f939..8fac36f4890 100644 --- a/packages/spec/src/ui/view.form.ts +++ b/packages/spec/src/ui/view.form.ts @@ -61,10 +61,10 @@ export const viewForm = defineForm({ ], }, // `pagination` is NOT grid-only: every view type accepts it, and - // `pagination.pageSize` is the one row bound a view carries (maintainer - // ruling D on #19228) — for a kanban, gallery or timeline view the only - // one. Its own section with no `visibleWhen` puts it in front of every - // type; inside `table_options` a non-grid author could not reach it. + // `pagination.pageSize` is the row bound every view type carries (maintainer + // ruling D on #19228 makes it the direction for a view's row bound). Its own + // section with no `visibleWhen` puts it in front of every type; inside + // `table_options` a non-grid author could not reach it. { name: 'pagination', label: 'Pagination',