diff --git a/.changeset/17393-view-row-ceiling.md b/.changeset/17393-view-row-ceiling.md deleted file mode 100644 index 675b2c05b0f..00000000000 --- a/.changeset/17393-view-row-ceiling.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -Gallery, kanban and timeline view configs declare an author-settable row ceiling. - -`GalleryConfigSchema`, `KanbanConfigSchema` and `TimelineConfigSchema` each gain a -`limit` member — a positive integer, default **100** — saying how many records the -view fetches. The default is APPLIED by the schema rather than only described, and -the key's own text states the other half of the contract: when the ceiling applies, -the renderer must show a visible truncation signal, because a bounded view that -looks complete is worse than an unbounded one. `DEFAULT_VIEW_ROW_LIMIT` is exported -so a consumer reads that number instead of re-declaring it. - -The knob belongs in the protocol because two renderers already cap by author choice -off keys the protocol never declared: objectui's kanban board fetches -`$top: schema.limit ?? DEFAULT_KANBAN_LIMIT` with `limit` declared in -`@object-ui/types` alone, its timeline does the same off a component props -interface, and its gallery caps not at all. `limit` is the name those consumers -already read, so this declaration absorbs the consumer-local keys instead of -introducing a second spelling of one concept. - -Nothing is removed, renamed or narrowed, and no document that parsed before is -refused now. Two things to know when upgrading: - -- a parsed gallery / kanban / timeline config carries `limit: 100` where the author - wrote no ceiling, so code that compares a parsed config against a literal object - sees the new member; -- `KanbanConfigParsed` is now declared (ADR-0122) because that schema has two shapes - for the first time; `KanbanConfig` is unchanged and remains the author state. - -The non-grid four — gantt, calendar, map and tree — are deliberately untouched: -their rows stay bounded by a platform ceiling the renderer owns, because a gantt's -range, a map's camera fit and a tree's parent chain are computed over the whole set. - -Clause-②: yes (widening) diff --git a/.changeset/19228-pagesize-fetch-ceiling.md b/.changeset/19228-pagesize-fetch-ceiling.md new file mode 100644 index 00000000000..d67a04006ca --- /dev/null +++ b/.changeset/19228-pagesize-fetch-ceiling.md @@ -0,0 +1,15 @@ +--- +'@objectstack/spec': patch +--- + +`pagination.pageSize` states what the renderer owes on a view with no pager + +On a kanban, gallery or timeline view there is no pager, so `pagination.pageSize` is the fetch +ceiling. Its description now says so, and names the renderer's two obligations there: bound the +fetch at that number, and, when the filtered set is larger than it, show a visible truncation +signal saying what is on screen is not the whole set. + +The key's accept set and its default (`25`) are unchanged, and no export or authorable key moves +relative to the last published release. + +Clause-②: no diff --git a/.changeset/19228-view-row-limit-route-record.md b/.changeset/19228-view-row-limit-route-record.md index 4693c232b48..bd49b28b240 100644 --- a/.changeset/19228-view-row-limit-route-record.md +++ b/.changeset/19228-view-row-limit-route-record.md @@ -2,12 +2,10 @@ "@objectstack/spec": patch --- -fix(spec): state the row-cap guard `ElementDataSourceGate` implements, and record where the per-kind view `limit` actually lands (#19228) +fix(spec): state the row-cap guard `ElementDataSourceGate` implements (#19228) Prose and pins only — zero accept-set movement, zero export movement. The same documents parse -to the same values before and after. ⛔ No `.default()` moves, ⛔ no precedence is picked: which -of the per-kind view `limit`, a view's `pagination.pageSize` and a component's flat `limit` -should win is the open half of #19228 and is not answered here. +to the same values before and after. ⛔ No `.default()` moves. ## What the published text said, and what an author can actually reach @@ -30,31 +28,5 @@ flat `view.limit` (`core/src/data-scope/element-data-source.ts:237-241`), but th saved-view RECORD as the adapter's `listViews()` returns it — a third face, not an authored view document. Measured on this tree: `ListViewSchema` REFUSES a flat `limit` with `unrecognized_keys: ["limit"]`, the verdict a bogus key gets, while the same minimal document -parses with `pagination.pageSize: 50` and with a per-kind `kanban.limit: 50`. No view document +parses with `pagination.pageSize: 50`. No view document declares a flat `limit` and none carries a tombstone for one. - -## Where the per-kind VIEW `limit` lands - -⚠️ Two different keys are easy to confuse here, so each statement names its face. The **view -face** is a `ListViewSchema` document's `kanban` / `gallery` / `timeline` block — that is where -this key lives. The **element face** is a page component node's own flat `limit`, declared in -`component.zod.ts`, and that is the key every renderer actually reads. An adapter turns the -first into the second. - -The adapters spread a view's per-kind block FLAT onto the node they generate — `...restKanban` -(`plugin-list/src/ListView.tsx:2979`, `plugin-view/src/ObjectView.tsx:1638`; neither destructure -strips `limit`) and `...(viewOptions.gallery || {})` / `...(viewOptions.timeline || {})` -(`ObjectView.tsx:1697` / `:1725`). So a view's `kanban.limit` — including the 100 the applied -default materializes — becomes the node's flat `limit`, which `ObjectKanban.tsx:553` reads. A -view's `timeline.limit` is route-dependent: `plugin-view` flattens it and `ObjectTimeline.tsx:279` -reads it, while `plugin-list` forwards the block nested, where nothing does. A view's -`gallery.limit` is flattened too and read by nobody — `ObjectGallery.tsx` contains no `limit` at -all. - -Where it is read, the `$top` it would govern is still not issued on either adapter route today, -because both hosts hand rows down as a React `data` prop and both children short-circuit their -own fetch; ⛔ that is a statement about the query, not about the key being unread. - -⚠️ For authors of an `object-timeline` NODE: a `limit` written inside that node's own `timeline` -block is read by no renderer on any route — the rail is capped by the flat `limit` beside it, -which is also the only one a bound `dataSource` lowers into. Write the flat one. diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 4fc30a4fb75..e578e76d0bf 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1674,7 +1674,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | | **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | -| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[]; … }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | | **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar configuration — applies when the view renders as a calendar layout | | **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | | **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | @@ -1759,7 +1759,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | | **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | -| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[]; … }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | | **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar configuration — applies when the view renders as a calendar layout | | **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | | **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index eb7035f05c1..ae52561ea28 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -373,7 +373,7 @@ const result = ApiMethod.parse(data); | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | | **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | -| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[]; … }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | | **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar configuration — applies when the view renders as a calendar layout | | **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | | **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 9219cdc48b9..dd412bb60f8 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -813,7 +813,7 @@ View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **objectName** | `string` | optional | Object this timeline binds to. Optional because the component-level `dataSource` binding can supply the object instead — this block registers through `ElementDataSourceGate`, which lowers the binding onto this key before the renderer sees the node | -| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline configuration, the author face — the same block `ListViewSchema.timeline` declares: `{ startDateField, endDateField, titleField, groupByField, colorField, scale, limit }`. The flat top-level spellings beside it are the runtime handoff, not a second authoring spelling. ⚠️ `limit` is the one member of this block NO renderer reads on any route: the rail is capped by the FLAT `limit` beside this key, which is also the only one a bound `dataSource` lowers into. A `limit` written inside this block is accepted, defaulted to 100, and never read | +| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline configuration, the author face — the same block `ListViewSchema.timeline` declares: `{ startDateField, endDateField, titleField, groupByField, colorField, scale }`. The flat top-level spellings beside it are the runtime handoff, not a second authoring spelling | | **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Row order for the fetched entries — the SortItem array form `[{ field, order }, ...]`, the one sort orthography every declared `sort` door on this platform shares; lowered to the wire `$orderby`. The legacy string clause (`name desc`) is refused — see migration `object-block-sort-item-array` | | **limit** | `integer` | optional | Maximum number of records loaded onto the rail (row cap); lowered to the query's top-level `$top` (renderer default 100). A timeline renders one rail with no pagination control, so this is the author's window rather than a page size | @@ -838,7 +838,6 @@ View filter rule | **groupByField** | `string` | optional | Field to group timeline rows. NO leading or trailing whitespace: the renderer reads this name off every row verbatim, so a padded spelling drops every row into one ungrouped band. | | **colorField** | `string` | optional | Field to derive each item color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the timeline default marker color | | **scale** | `Enum<'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional (default: `"week"`) | Default timeline scale | -| **limit** | `integer` | optional (default: `100`) | Row ceiling — the most rows the timeline fetches onto its rail; default 100 when the key is absent. The renderer owes two things: bound its fetch at this number, and, when the ceiling APPLIES (the filtered set is larger than it), show a visible truncation signal saying what is on screen is not the whole set — a bounded view that looks complete is worse than an unbounded one. ⚠️ Not every view kind has a renderer that reads this key yet; which do is recorded on the declaration. | ### Nested Shape: `ObjectTimelineProps.filter[number]` diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 71078a81c47..4fc05a70ebb 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -545,7 +545,6 @@ Gallery/card view configuration | **cardSize** | `Enum<'small' \| 'medium' \| 'large'>` | optional (default: `"medium"`) | Card size in gallery view | | **titleField** | `string` | optional | Field to display as card title | | **visibleFields** | `string[]` | optional | Fields to display on card body | -| **limit** | `integer` | optional (default: `100`) | Row ceiling — the most cards the gallery fetches and draws; default 100 when the key is absent. The renderer owes two things: bound its fetch at this number, and, when the ceiling APPLIES (the filtered set is larger than it), show a visible truncation signal saying what is on screen is not the whole set — a bounded view that looks complete is worse than an unbounded one. ⚠️ Not every view kind has a renderer that reads this key yet; which do is recorded on the declaration. | --- @@ -701,7 +700,6 @@ HTTP methods a view data source may request — the subset of `HttpMethod` witho | **summarizeField** | `string` | optional | Field to sum at top of column (e.g. amount) | | **titleField** | `string` | optional | Field displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **columns** | `string[]` | ✅ | Fields to show on cards | -| **limit** | `integer` | optional (default: `100`) | Row ceiling — the most records the board fetches across all its lanes; default 100 when the key is absent. The renderer owes two things: bound its fetch at this number, and, when the ceiling APPLIES (the filtered set is larger than it), show a visible truncation signal saying what is on screen is not the whole set — a bounded view that looks complete is worse than an unbounded one. ⚠️ Not every view kind has a renderer that reads this key yet; which do is recorded on the declaration. | --- @@ -803,7 +801,7 @@ Map view configuration | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | | **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | -| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[]; … }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | | **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar configuration — applies when the view renders as a calendar layout | | **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | | **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | @@ -929,7 +927,7 @@ View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **pageSize** | `integer` | optional (default: `25`) | Number of records per page | +| **pageSize** | `integer` | optional (default: `25`) | Number of records per page. On a view with no pager (kanban, gallery, timeline) it is the fetch ceiling, and the renderer owes two things: bound its fetch at this number, and, when the filtered set is larger than it, show a visible truncation signal saying what is on screen is not the whole set | | **pageSizeOptions** | `integer[]` | optional | Available page size options | ### Nested Shape: `ListView.kanban` @@ -940,7 +938,6 @@ View filter rule | **summarizeField** | `string` | optional | Field to sum at top of column (e.g. amount) | | **titleField** | `string` | optional | Field displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **columns** | `string[]` | ✅ | Fields to show on cards | -| **limit** | `integer` | optional (default: `100`) | Row ceiling — the most records the board fetches across all its lanes; default 100 when the key is absent. The renderer owes two things: bound its fetch at this number, and, when the ceiling APPLIES (the filtered set is larger than it), show a visible truncation signal saying what is on screen is not the whole set — a bounded view that looks complete is worse than an unbounded one. ⚠️ Not every view kind has a renderer that reads this key yet; which do is recorded on the declaration. | ### Nested Shape: `ListView.calendar` @@ -995,7 +992,6 @@ View filter rule | **cardSize** | `Enum<'small' \| 'medium' \| 'large'>` | optional (default: `"medium"`) | Card size in gallery view | | **titleField** | `string` | optional | Field to display as card title | | **visibleFields** | `string[]` | optional | Fields to display on card body | -| **limit** | `integer` | optional (default: `100`) | Row ceiling — the most cards the gallery fetches and draws; default 100 when the key is absent. The renderer owes two things: bound its fetch at this number, and, when the ceiling APPLIES (the filtered set is larger than it), show a visible truncation signal saying what is on screen is not the whole set — a bounded view that looks complete is worse than an unbounded one. ⚠️ Not every view kind has a renderer that reads this key yet; which do is recorded on the declaration. | ### Nested Shape: `ListView.timeline` @@ -1007,7 +1003,6 @@ View filter rule | **groupByField** | `string` | optional | Field to group timeline rows. NO leading or trailing whitespace: the renderer reads this name off every row verbatim, so a padded spelling drops every row into one ungrouped band. | | **colorField** | `string` | optional | Field to derive each item color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the timeline default marker color | | **scale** | `Enum<'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional (default: `"week"`) | Default timeline scale | -| **limit** | `integer` | optional (default: `100`) | Row ceiling — the most rows the timeline fetches onto its rail; default 100 when the key is absent. The renderer owes two things: bound its fetch at this number, and, when the ceiling APPLIES (the filtered set is larger than it), show a visible truncation signal saying what is on screen is not the whole set — a bounded view that looks complete is worse than an unbounded one. ⚠️ Not every view kind has a renderer that reads this key yet; which do is recorded on the declaration. | ### Nested Shape: `ListView.chart` @@ -1214,7 +1209,7 @@ Tab configuration for multi-tab view interface | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | | **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | -| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[]; … }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | | **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar configuration — applies when the view renders as a calendar layout | | **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | | **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | @@ -1331,7 +1326,7 @@ View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **pageSize** | `integer` | optional (default: `25`) | Number of records per page | +| **pageSize** | `integer` | optional (default: `25`) | Number of records per page. On a view with no pager (kanban, gallery, timeline) it is the fetch ceiling, and the renderer owes two things: bound its fetch at this number, and, when the filtered set is larger than it, show a visible truncation signal saying what is on screen is not the whole set | | **pageSizeOptions** | `integer[]` | optional | Available page size options | ### Nested Shape: `ObjectListView.kanban` @@ -1342,7 +1337,6 @@ View filter rule | **summarizeField** | `string` | optional | Field to sum at top of column (e.g. amount) | | **titleField** | `string` | optional | Field displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **columns** | `string[]` | ✅ | Fields to show on cards | -| **limit** | `integer` | optional (default: `100`) | Row ceiling — the most records the board fetches across all its lanes; default 100 when the key is absent. The renderer owes two things: bound its fetch at this number, and, when the ceiling APPLIES (the filtered set is larger than it), show a visible truncation signal saying what is on screen is not the whole set — a bounded view that looks complete is worse than an unbounded one. ⚠️ Not every view kind has a renderer that reads this key yet; which do is recorded on the declaration. | ### Nested Shape: `ObjectListView.calendar` @@ -1397,7 +1391,6 @@ View filter rule | **cardSize** | `Enum<'small' \| 'medium' \| 'large'>` | optional (default: `"medium"`) | Card size in gallery view | | **titleField** | `string` | optional | Field to display as card title | | **visibleFields** | `string[]` | optional | Fields to display on card body | -| **limit** | `integer` | optional (default: `100`) | Row ceiling — the most cards the gallery fetches and draws; default 100 when the key is absent. The renderer owes two things: bound its fetch at this number, and, when the ceiling APPLIES (the filtered set is larger than it), show a visible truncation signal saying what is on screen is not the whole set — a bounded view that looks complete is worse than an unbounded one. ⚠️ Not every view kind has a renderer that reads this key yet; which do is recorded on the declaration. | ### Nested Shape: `ObjectListView.timeline` @@ -1409,7 +1402,6 @@ View filter rule | **groupByField** | `string` | optional | Field to group timeline rows. NO leading or trailing whitespace: the renderer reads this name off every row verbatim, so a padded spelling drops every row into one ungrouped band. | | **colorField** | `string` | optional | Field to derive each item color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the timeline default marker color | | **scale** | `Enum<'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional (default: `"week"`) | Default timeline scale | -| **limit** | `integer` | optional (default: `100`) | Row ceiling — the most rows the timeline fetches onto its rail; default 100 when the key is absent. The renderer owes two things: bound its fetch at this number, and, when the ceiling APPLIES (the filtered set is larger than it), show a visible truncation signal saying what is on screen is not the whole set — a bounded view that looks complete is worse than an unbounded one. ⚠️ Not every view kind has a renderer that reads this key yet; which do is recorded on the declaration. | ### Nested Shape: `ObjectListView.chart` @@ -1603,7 +1595,7 @@ Quick-filter field configuration | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **pageSize** | `integer` | optional (default: `25`) | Number of records per page | +| **pageSize** | `integer` | optional (default: `25`) | Number of records per page. On a view with no pager (kanban, gallery, timeline) it is the fetch ceiling, and the renderer owes two things: bound its fetch at this number, and, when the filtered set is larger than it, show a visible truncation signal saying what is on screen is not the whole set | | **pageSizeOptions** | `integer[]` | optional | Available page size options | @@ -1663,7 +1655,6 @@ Timeline view configuration | **groupByField** | `string` | optional | Field to group timeline rows. NO leading or trailing whitespace: the renderer reads this name off every row verbatim, so a padded spelling drops every row into one ungrouped band. | | **colorField** | `string` | optional | Field to derive each item color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the timeline default marker color | | **scale** | `Enum<'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional (default: `"week"`) | Default timeline scale | -| **limit** | `integer` | optional (default: `100`) | Row ceiling — the most rows the timeline fetches onto its rail; default 100 when the key is absent. The renderer owes two things: bound its fetch at this number, and, when the ceiling APPLIES (the filtered set is larger than it), show a visible truncation signal saying what is on screen is not the whole set — a bounded view that looks complete is worse than an unbounded one. ⚠️ Not every view kind has a renderer that reads this key yet; which do is recorded on the declaration. | --- @@ -1817,7 +1808,7 @@ Tab configuration for multi-tab view interface | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | | **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | -| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[]; … }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | | **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar configuration — applies when the view renders as a calendar layout | | **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | | **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | @@ -1902,7 +1893,7 @@ Tab configuration for multi-tab view interface | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | | **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | -| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[]; … }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | | **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar configuration — applies when the view renders as a calendar layout | | **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | | **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | @@ -2143,7 +2134,7 @@ This schema accepts one of the following structures: | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | | **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | -| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[]; … }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | | **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar configuration — applies when the view renders as a calendar layout | | **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | | **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | @@ -2319,7 +2310,7 @@ This schema accepts one of the following structures: | **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) | | **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration | -| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[]; … }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | +| **kanban** | `{ groupByField: string; summarizeField?: string; titleField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout | | **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar configuration — applies when the view renders as a calendar layout | | **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout | | **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration | diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index 2f8c1fcd51d..d90dcff4eee 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -107,7 +107,6 @@ "ComponentPropsMap (const)", "DATE_RANGE_DEFAULT_RANGES (const)", "DATE_RANGE_PRESETS (const)", - "DEFAULT_VIEW_ROW_LIMIT (const)", "Dashboard (const)", "Dashboard (type)", "DashboardHeader (type)", @@ -221,7 +220,6 @@ "KNOWN_COMPONENT_TYPES (const)", "KNOWN_COMPONENT_TYPE_CANDIDATES (const)", "KanbanConfig (type)", - "KanbanConfigParsed (type)", "KanbanConfigSchema (const)", "LIST_VIEW_GROUP_COUNT_ALIAS (const)", "ListChartConfig (type)", diff --git a/packages/spec/authorable-defaults/ui.json b/packages/spec/authorable-defaults/ui.json index 006c5cd51a1..e4627338c7f 100644 --- a/packages/spec/authorable-defaults/ui.json +++ b/packages/spec/authorable-defaults/ui.json @@ -50,7 +50,6 @@ "ui/FormView:type = \"simple\"", "ui/GalleryConfig:cardSize = \"medium\"", "ui/GalleryConfig:coverFit = \"cover\"", - "ui/GalleryConfig:limit = 100", "ui/GlobalFilter:scope = \"dashboard\"", "ui/GroupNavItem:expanded = false", "ui/GroupingField:collapsed = false", @@ -59,7 +58,6 @@ "ui/InlineAction:refreshAfter = false", "ui/InlineAction:type = \"script\"", "ui/JoinedReportBlock:type = \"tabular\"", - "ui/KanbanConfig:limit = 100", "ui/ListChartConfig:chartType = \"bar\"", "ui/ListView:type = \"grid\"", "ui/NavigationConfig:mode = \"page\"", @@ -108,7 +106,6 @@ "ui/SelectionConfig:type = \"none\"", "ui/SharingConfig:allowAnonymous = false", "ui/SharingConfig:enabled = false", - "ui/TimelineConfig:limit = 100", "ui/TimelineConfig:scale = \"week\"", "ui/UrlNavItem:target = \"_self\"", "ui/UserActionsConfig:addRecordForm = false", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index e1e58ea7329..bb7892953f6 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -514,7 +514,6 @@ "ui/GalleryConfig:cardSize", "ui/GalleryConfig:coverField", "ui/GalleryConfig:coverFit", - "ui/GalleryConfig:limit", "ui/GalleryConfig:titleField", "ui/GalleryConfig:visibleFields", "ui/GanttConfig:assigneeField", @@ -624,7 +623,6 @@ "ui/JoinedReportBlock:values", "ui/KanbanConfig:columns", "ui/KanbanConfig:groupByField", - "ui/KanbanConfig:limit", "ui/KanbanConfig:summarizeField", "ui/KanbanConfig:titleField", "ui/ListChartConfig:chartType", @@ -1198,7 +1196,6 @@ "ui/TimelineConfig:colorField", "ui/TimelineConfig:endDateField", "ui/TimelineConfig:groupByField", - "ui/TimelineConfig:limit", "ui/TimelineConfig:scale", "ui/TimelineConfig:startDateField", "ui/TimelineConfig:titleField", diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index 3cee541f645..b9e4ff35012 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -104,7 +104,6 @@ "ComponentPropsMap": "src/ui/component.zod.ts#ComponentPropsMap (const)", "DATE_RANGE_DEFAULT_RANGES": "src/ui/dashboard.zod.ts#DATE_RANGE_DEFAULT_RANGES (const)", "DATE_RANGE_PRESETS": "src/data/date-range-presets.ts#DATE_RANGE_PRESETS (const)", - "DEFAULT_VIEW_ROW_LIMIT": "src/ui/view.zod.ts#DEFAULT_VIEW_ROW_LIMIT (const)", "Dashboard": "src/ui/dashboard.zod.ts#Dashboard (type)", "DashboardHeader": "src/ui/dashboard.zod.ts#DashboardHeader (type)", "DashboardHeaderAction": "src/ui/dashboard.zod.ts#DashboardHeaderAction (type)", @@ -217,7 +216,6 @@ "KNOWN_COMPONENT_TYPES": "src/ui/component-type-vocabulary.ts#KNOWN_COMPONENT_TYPES (const)", "KNOWN_COMPONENT_TYPE_CANDIDATES": "src/ui/component-type-vocabulary.ts#KNOWN_COMPONENT_TYPE_CANDIDATES (const)", "KanbanConfig": "src/ui/view.zod.ts#KanbanConfig (type)", - "KanbanConfigParsed": "src/ui/view.zod.ts#KanbanConfigParsed (type)", "KanbanConfigSchema": "src/ui/view.zod.ts#KanbanConfigSchema (const)", "LIST_VIEW_GROUP_COUNT_ALIAS": "src/ui/view-grouping-query.ts#LIST_VIEW_GROUP_COUNT_ALIAS (const)", "ListChartConfig": "src/ui/view.zod.ts#ListChartConfig (type)", diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 8ae4405bd57..8debed6e2f9 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -275,7 +275,7 @@ import type * as M187 from './shared/duration.zod.js'; import type * as M188 from './ai/build-progress.zod.js'; // --------------------------------------------------------------------------- -// 789 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 790 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. // // That number is machine-checked, not hand-kept. The runtime companion at the // bottom of this file recomputes the pin count from the source and asserts that @@ -1570,18 +1570,17 @@ export type Iso_ui_responsive__StyleMapSchema = Assert, z.infer< typeof M167.CalendarConfigSchema > >>; export type Iso_ui_view__ColumnSummaryConfigSchema = Assert, z.infer< typeof M167.ColumnSummaryConfigSchema > >>; export type Iso_ui_view__ColumnSummarySchema = Assert, z.infer< typeof M167.ColumnSummarySchema > >>; export type Iso_ui_view__FormButtonConfigSchema = Assert, z.infer< typeof M167.FormButtonConfigSchema > >>; export type Iso_ui_view__GanttConfigSchema = Assert, z.infer< typeof M167.GanttConfigSchema > >>; export type Iso_ui_view__GanttQuickFilterSchema = Assert, z.infer< typeof M167.GanttQuickFilterSchema > >>; +export type Iso_ui_view__KanbanConfigSchema = Assert, z.infer< typeof M167.KanbanConfigSchema > >>; export type Iso_ui_view__ListMapConfigSchema = Assert, z.infer< typeof M167.ListMapConfigSchema > >>; export type Iso_ui_view__NavigationModeSchema = Assert, z.infer< typeof M167.NavigationModeSchema > >>; export type Iso_ui_view__RowColorConfigSchema = Assert, z.infer< typeof M167.RowColorConfigSchema > >>; @@ -1663,7 +1662,7 @@ describe('ADR-0122 type-alias convention', () => { // this title and the section header above the pin list — are now asserted // against the recomputed count below, so neither can go stale without a red // test naming it. - it('still declares all 789 isomorphic pins', () => { + it('still declares all 790 isomorphic pins', () => { // The truth of each pin is proved by tsc, not here — an `Assert>` // that stops holding is a compile error with the alias named. What tsc // cannot notice is a pin that was DELETED: removing the assertion removes @@ -2281,7 +2280,12 @@ describe('ADR-0122 type-alias convention', () => { // true of the file when its entry was written, like the counts beside it. // The set `check:spec-parsed-alias` reads is identical member for member, // and so is each assertion — only the names moved. +0. - expect(pins).toHaveLength(789); + // + // 789 -> 790 is #19228's removal of that same row ceiling before any + // release carried it: `KanbanConfigSchema` loses the `limit` whose applied + // default had split its two shapes, `KanbanConfigParsed` is deleted and the + // schema is re-pinned as Iso_ui_view__KanbanConfigSchema. +1 added. + expect(pins).toHaveLength(790); // The count is stated in PROSE twice as well — this case's title and the // section header above the pin list — and until #6605 nothing read either diff --git a/packages/spec/src/ui/component.test.ts b/packages/spec/src/ui/component.test.ts index dbea5c52674..516cb66345a 100644 --- a/packages/spec/src/ui/component.test.ts +++ b/packages/spec/src/ui/component.test.ts @@ -26,7 +26,7 @@ import { import { PageComponentSchema, PageSchema, PageComponentType, ElementDataSourceSchema, RETIRED_PAGE_COMPONENT_TYPES } from './page.zod'; import { GanttConfigSchema, TreeConfigSchema, ListMapConfigSchema, ListColumnSchema, ListViewSchema, - TimelineConfigSchema, DEFAULT_VIEW_ROW_LIMIT, + TimelineConfigSchema, } from './view.zod'; import { FieldSchema } from '../data/field.zod'; import { ALL_CONVERSIONS } from '../conversions/registry'; @@ -3882,11 +3882,9 @@ describe('the three #18305 object blocks — key sets derived from the renderers }); }); -// #19228 — two authorable row bounds land on one `object-timeline` node, and -// the react tier's own precedence sentence was narrower than the guard it -// names. ⛔ This card picks NO precedence and changes no `.default()`; these -// pins only hold the two structural facts the repair rests on, measured -// first-hand at the objectui pin `87af769e9` on 2026-09-21T06:30-06:40Z. +// #19228 — the react tier's own precedence sentence was narrower than the +// guard it names. These pins hold the structural facts the repair rests on, +// measured first-hand at the objectui pin `87af769e9` on 2026-09-21T06:30-06:40Z. describe('row caps on the object-bound blocks — what #19228 recorded', () => { const timeline = ComponentPropsMap['object-timeline']; const kanban = ComponentPropsMap['object-kanban']; @@ -3904,36 +3902,10 @@ describe('row caps on the object-bound blocks — what #19228 recorded', () => { } // LIT CONTROL, same instrument (a Zod applied default, observed through - // `parse`): the VIEW-face sibling DOES materialize one, so the zeros above - // are a reading rather than a parse that never ran. - const viewSide = TimelineConfigSchema.parse({ startDateField: 'start_date', titleField: 'name' }) as { limit?: number }; - expect(viewSide.limit).toBe(DEFAULT_VIEW_ROW_LIMIT); - }); - - it('materializes the NESTED `timeline.limit` on a node whose flat `limit` stays absent', () => { - // The shape the record is about: one strictObject, two authorable row - // caps, and an applied default on the nested one. ⚠️ Faces, because this - // card keeps confusing them: the NESTED key asserted below is the ELEMENT - // face, and at the pin no renderer reads it on any route. The - // route-dependent one is a VIEW document's `timeline.limit`, a different - // key on a different document, which `ObjectView.tsx:1725` flattens onto - // a generated node's FLAT `limit`. Neither is asserted here: this pin is - // about the PARSE, which is the only half a schema owns. - const result = timeline.safeParse({ - objectName: 'task', - timeline: { startDateField: 'start_date', titleField: 'name' }, - }); - expect(result.success).toBe(true); - const data = (result.success ? result.data : undefined) as - { limit?: unknown; timeline?: { limit?: unknown } } | undefined; - expect(data?.timeline?.limit).toBe(DEFAULT_VIEW_ROW_LIMIT); - expect(Object.prototype.hasOwnProperty.call(data ?? {}, 'limit')).toBe(false); - - // CONTROL — the node is still strict, so the acceptance above is not the - // verdict of a map that has stopped refusing anything. - const control = timeline.safeParse({ objectName: 'task', zzUnlikelyBogusKey__: 1 }); - expect(control.success).toBe(false); - expect(JSON.stringify(control.error?.issues)).toContain('unrecognized_keys'); + // `parse`): a VIEW-face block DOES materialize its `scale`, so the zeros + // above are a reading rather than a parse that never ran. + const viewSide = TimelineConfigSchema.parse({ startDateField: 'start_date', titleField: 'name' }) as { scale?: string }; + expect(viewSide.scale).toBe('week'); }); it('admits only caps the binding gate calls usable — the SUBSET that makes 「unset」 the whole rule', () => { diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 3b788891c78..f9c08f74a17 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -3145,7 +3145,11 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({ * `objectql.ts:3735` -> `:3832`, `ElementDataSourceGate.tsx:316-331` -> * `:373-388` and `:192-194` -> `:200-202`, `ListView.tsx:2979` -> `:3067` and * `:2952` -> `:3040`, `ObjectView.tsx:1638` -> `:1666` and `:1579` -> - * `:1607` — and `ObjectKanban.tsx:10` did not move. Every file here was + * `:1607` — and `ObjectKanban.tsx:10` did not move. (`:554`, `:565`, `:687` + * and the `ListView.tsx` / `ObjectView.tsx` pairs are that re-read's record + * only: they anchored the view-face `kanban.limit` spread, a key #19228 + * retired before any release carried it, so this block no longer cites + * them.) Every file here was * byte-identical across the hop onto `62597c588`. All four * anchors were re-READ at `87af769e9` 2026-09-20 — the hop that moved every * one of them, and RENAMED one face rather than shifting it), four @@ -3204,37 +3208,16 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({ * — not an authored view document. Measured on this tree: `ListViewSchema` * REFUSES a flat `limit` with `unrecognized_keys: ["limit"]`, the same * verdict a bogus key gets, while the same minimal document parses with - * `pagination.pageSize: 50` and with a per-kind `kanban.limit: 50`. There is + * `pagination.pageSize: 50`. There is * no flat `limit` member on any view document and no `retiredKey()` * tombstone for one. ⛔ So naming that arm here would put a runtime-record * shape on the author face with no qualifier — the face-merge this card has * now failed on three times. * ⛔ This note reports the guard; it picks no precedence. * - * ⚠️ It takes a THIRD door, and missing it is what the at-tier review of - * #19533 caught. Stated with its faces named, because that is where this - * card keeps going wrong: `kanban.limit` is a VIEW-FACE key (a member of a - * `ListViewSchema` document's `kanban` block) and THIS `limit` is the - * ELEMENT-FACE key on the node; the third door is the one that turns the - * first into the second. The ADAPTERS spread the view's kanban block FLAT - * onto the generated node — `plugin-list/src/ListView.tsx:3067` and - * `plugin-view/src/ObjectView.tsx:1666`, both `...restKanban`, and neither - * destructure (`ListView.tsx:3040`, `ObjectView.tsx:1607`) strips `limit`. - * So a view's `kanban.limit`, INCLUDING the 100 its applied default - * materializes, lands on THIS key, and `ObjectKanban.tsx:554` reads it - * (`describeRefusedRowLimit`, unconditional). The `$top` at `:687` is not - * issued on either route today — both hosts pass rows down as a React `data` - * prop and the board short-circuits at `:565` — so it governs no query - * there, which is ⛔ NOT the same claim as 「no consumer reads it」. A - * spread carries a key without spelling it, so a property-access sweep - * cannot see this and returns a confident zero. - * - * ⭐ The arm is nonetheless REACHABLE, for a reason the spreads do not - * touch: its guard reads THIS key on the node as AUTHORED, and this + * ⭐ The arm is REACHABLE: its guard reads THIS key on the node as AUTHORED, and this * declaration is `.optional()` with no applied default, so an author's - * silence is still silence at parse time. #19228 read the VIEW-face applied - * default as killing this arm; the two are different schemas, and what the - * adapters lower is a rendered node, not the parse of this one. ⛔ Do not add + * silence is still silence at parse time. ⛔ Do not add * a `.default()` here: that — and only that — is what would make it dead. * The board * has no `pagination` read point, so declaring that spelling here would name @@ -4242,13 +4225,7 @@ const OBJECT_TIMELINE_FLAT_CONFIG_GUIDANCE: readonly KeySetGuidance[] = [ * the shared `convertSortToQueryParams` sink, as `object-calendar`'s does), * `limit` (`:407`, the fetch's one top-level `$top`, through * `resolveRowLimit(schema.limit, DEFAULT_TIMELINE_LIMIT)` with the default - * `100` at `:29` and the refused-cap diagnostic at `:279`. ⚠️ It is this FLAT - * key that is read — this node's own nested `timeline.limit` is not, on any - * route. What reaches this flat key is a VIEW document's `timeline.limit`, - * flattened onto the generated node by `plugin-view/src/ObjectView.tsx:1725` - * (`...(viewOptions.timeline || {})`, spelled with its package because objectui - * carries a second `ObjectView.tsx` in `app-shell`); see the - * `timeline` door below), `items` (`:254`, `:420`, `:487`, + * `100` at `:29` and the refused-cap diagnostic at `:279`), `items` (`:254`, `:420`, `:487`, * `:668` — the authored pass-through that short-circuits the object query), * `data` (`:255`, `:420`, `:442`, `:444` — the pre-fetched record source, * read off REACT PROPS rather than `schema`; the door's own docblock carries @@ -4280,53 +4257,6 @@ const OBJECT_TIMELINE_FLAT_CONFIG_GUIDANCE: readonly KeySetGuidance[] = [ * VALUE posture: `timeline` takes {@link TimelineConfigSchema}, the block * `ListViewSchema.timeline` already declares — one vocabulary, taken by * reference, so this element face cannot fork from the view face. - * ⚠️ Taking it by reference also imported #19226's new `limit` onto THIS - * strictObject, beside the flat `limit` below — two authorable row caps on one - * node, and the nested one carries an APPLIED default, so every parsed node - * with a `timeline` block materializes `timeline.limit: 100` (#19228). - * - * ⛔ THE TWO ARE ON DIFFERENT FACES, and #19228 has now gone wrong on that - * boundary in both directions — once putting a view-face fact on the element - * key, once the reverse. So each statement names its face first. Measured at - * the pin `87af769e9`, 2026-09-21T09:15Z: - * - **ELEMENT FACE — THIS node's own nested `timeline.limit`: read on NO - * route.** `ObjectTimeline` binds `timelineConfig = schema.timeline` - * (`ObjectTimeline.tsx:262`) and reads exactly eight members off it — - * `startDateField`, `dateField`, `titleField`, `endDateField`, - * `groupByField`, `colorField`, `metaFields` (`:520`, through an `as any` - * cast, which is why a `timelineConfig?.x` sweep alone under-counts) and - * `scale`. `limit` is not among them, and NO spread of a node's own - * `timeline` block exists anywhere in objectui (0 hits on the NODE face — - * method: `git grep '\.\.\.(schema\.timeline'` at this pin returns four - * lines and every one is on the VIEW face, where `schema` is the view - * document: the block spread at `plugin-list/src/ListView.tsx:3066` and the - * three member-conditional ones at `:3114-3116`. ⛔ Without that scope - * written beside it the count is not 0. Lit control, same shape: the - * view-block spreads `...mergedTimeline` / - * `...(viewOptions.timeline || {})`, which do fire). The - * `ElementDataSourceGate` mapping is `limit: 'limit'` - * (`plugin-timeline/src/index.tsx:333`) — FLAT, so it never touches the - * nested key either. - * - **VIEW FACE — a `ListViewSchema` document's `timeline.limit`: that is - * the route-dependent one**, and it is a different key on a different - * document. `plugin-view/src/ObjectView.tsx:1725` flattens the view block - * onto the node it returns (which then carries no `timeline` block at all), - * so the value arrives as this node's FLAT `limit` and is read; - * `ListView.tsx:3084` forwards it nested instead, where nothing reads it. - * The package is spelled because objectui carries a second `ObjectView.tsx` - * (in `app-shell`), where that same line number is an unrelated `catch` — - * a bare spelling here names neither file. Recorded on - * `rowLimitKey` in `view.zod.ts`, which is where that key lives. - * - * ⚠️ Flagged, not fixed — a THIRD route neither the card nor the first two - * reviews described: a hand-authored `object-timeline` node reaching - * `ObjectTimeline` through `SchemaRenderer` with no adapter in between. Its - * nested `timeline.limit` is unread there too (it is the same element-face - * key as the first bullet), and that route is the one an AUTHOR face is - * written for — so it is the route the open question most concerns. - * - * ⛔ Recorded, not repaired: which key should carry a timeline's row cap is - * the open half of #19228. * `mapping` stays `z.unknown()`: its contract * (`TimelineMappingSchema`) still lives in objectui, which is the * `object-calendar.calendar` posture this section's header prescribes for @@ -4357,7 +4287,7 @@ export const ObjectTimelinePropsSchema = lazySchema(() => strictObject({ objectName: z.string().optional() .describe('Object this timeline binds to. Optional because the component-level `dataSource` binding can supply the object instead — this block registers through `ElementDataSourceGate`, which lowers the binding onto this key before the renderer sees the node'), timeline: TimelineConfigSchema.optional() - .describe('Timeline configuration, the author face — the same block `ListViewSchema.timeline` declares: { startDateField, endDateField, titleField, groupByField, colorField, scale, limit }. The flat top-level spellings beside it are the runtime handoff, not a second authoring spelling. ⚠️ `limit` is the one member of this block NO renderer reads on any route: the rail is capped by the FLAT `limit` beside this key, which is also the only one a bound `dataSource` lowers into. A `limit` written inside this block is accepted, defaulted to 100, and never read'), + .describe('Timeline configuration, the author face — the same block `ListViewSchema.timeline` declares: { startDateField, endDateField, titleField, groupByField, colorField, scale }. The flat top-level spellings beside it are the runtime handoff, not a second authoring spelling'), /** Base query filter — the family's one `ViewFilterRule` array orthography (#15449). */ filter: z.array(ViewFilterRuleSchema, { error: ruleArrayFilterError({ diff --git a/packages/spec/src/ui/view.test.ts b/packages/spec/src/ui/view.test.ts index 2181b8045eb..01e611520e7 100644 --- a/packages/spec/src/ui/view.test.ts +++ b/packages/spec/src/ui/view.test.ts @@ -45,7 +45,6 @@ import { ViewMetadataSchema, VIEW_METADATA_MEMBERS, TreeConfigSchema, - DEFAULT_VIEW_ROW_LIMIT, } from './view.zod'; import { @@ -4793,114 +4792,34 @@ describe('ListViewSchema — `viewType` is not a spelling of `type` (#16577)', ( // ============================================================================ -// [#17393] The author-settable row ceiling on the page-shaped view configs. -// -// A protocol-first card: objectui caps kanban and timeline by author choice off -// a key `@objectstack/spec` never declared (`$top: schema.limit ?? DEFAULT_*_LIMIT`), -// and the gallery — the third page-shaped view — caps not at all. These pins -// hold the new declaration to the three things a ceiling has to be: APPLIED -// (the default the prose states is the default the parse produces), BOUNDED -// (a value that could not cap a fetch is refused by name), and SCOPED (the -// non-grid four keep objectui#7210's platform ceiling and do not gain an -// authorable one). +// [#19228] One row bound per view: `pagination.pageSize`. No per-kind view +// config declares a `limit` of its own, so a view never carries two row bounds +// with no declared precedence between them. // ============================================================================ -describe('view row ceiling — `limit` on the page-shaped view configs (#17393)', () => { - /** - * One minimal, parse-clean block per page-shaped config, so every verdict - * below is about `limit` alone rather than about a missing sibling key. - */ - const PAGE_SHAPED = [ - ['gallery', GalleryConfigSchema as unknown as z.ZodTypeAny, {}], - ['kanban', KanbanConfigSchema as unknown as z.ZodTypeAny, { groupByField: 'status', columns: ['name'] }], - ['timeline', TimelineConfigSchema as unknown as z.ZodTypeAny, { startDateField: 'start_date', titleField: 'name' }], - ] as const; - - /** The `limit` member's own `.describe()` text, off the built shape. */ - const describeOf = (schema: z.ZodTypeAny): string => - (schema as unknown as { shape: Record }).shape.limit?.description ?? ''; - - it('applies the ceiling it declares when the author writes none', () => { - for (const [label, schema, minimal] of PAGE_SHAPED) { - const parsed = schema.parse({ ...minimal }) as { limit?: unknown }; - expect(parsed.limit, label).toBe(DEFAULT_VIEW_ROW_LIMIT); - } - }); - - it('accepts an authored ceiling as a MEMBER, with both controls firing on the same shape', () => { - for (const [label, schema, minimal] of PAGE_SHAPED) { - // CONTROL-1 — this surface CAN refuse a key, so acceptance below means something. - const control = schema.safeParse({ ...minimal, zzUnlikelyBogusKey__: 7 }); - expect(control.success, label).toBe(false); - expect(JSON.stringify((control as { error?: z.ZodError }).error?.issues), label) - .toContain('unrecognized_keys'); - - // CONTROL-2 — the refusal is about the NAME: the same block without it parses. - expect(schema.safeParse({ ...minimal }).success, label).toBe(true); - - // PROBE — the authored value SURVIVES the parse; it is not merely tolerated. - const probe = schema.safeParse({ ...minimal, limit: 25 }); - expect(probe.success, label).toBe(true); - expect(((probe as { data?: { limit?: unknown } }).data)?.limit, label).toBe(25); - } - }); - - it('refuses a value that could not bound a fetch — and refuses it BY NAME', () => { - for (const [label, schema, minimal] of PAGE_SHAPED) { - for (const bad of [0, -1, 2.5, '100', null] as const) { - const at = `${label} limit=${JSON.stringify(bad)}`; - const result = schema.safeParse({ ...minimal, limit: bad }); - expect(result.success, at).toBe(false); - const issues = (result as { error?: z.ZodError }).error?.issues ?? []; - expect(issues.some((issue) => issue.path[0] === 'limit'), `${at}: ${JSON.stringify(issues)}`) - .toBe(true); - } - } - }); - - it('states the default it ACTUALLY applies — prose and schema pinned to each other', () => { - for (const [label, schema, minimal] of PAGE_SHAPED) { - const description = describeOf(schema); - const stated = /default (\d+)/.exec(description); - expect(stated, `${label}: ${description}`).not.toBeNull(); - const applied = (schema.parse({ ...minimal }) as { limit: number }).limit; - expect(Number(stated?.[1]), `${label}: ${description}`).toBe(applied); - } - }); - - it('tells the author the renderer owes a VISIBLE truncation signal', () => { - // ⛔ The signal itself is the renderer's half and cannot be enforced from a - // schema. What the protocol can do — and what objectui#7390's ruling turns - // on — is say the cap is owed a signal, so that "bounded and silent" is - // never read as the finished job. - for (const [label, schema] of PAGE_SHAPED) { - expect(describeOf(schema), label).toContain('visible truncation signal'); - } - }); - - it('leaves the non-grid four WITHOUT an authorable ceiling (objectui#7210 keeps theirs)', () => { - // The card scopes those four out by name: their rows are capped by a - // platform constant the renderer owns, because a gantt range, a map camera - // fit and a tree parent chain are computed over the whole set. An - // authorable ceiling there would be surface no renderer reads. - const NON_GRID_FOUR = [ +describe('view row bound — no per-kind `limit` on the view configs (#19228)', () => { + it('refuses `limit` BY NAME on each per-kind view config', () => { + const PER_KIND = [ + ['gallery', GalleryConfigSchema as unknown as z.ZodTypeAny], + ['kanban', KanbanConfigSchema as unknown as z.ZodTypeAny], + ['timeline', TimelineConfigSchema as unknown as z.ZodTypeAny], ['gantt', GanttConfigSchema as unknown as z.ZodTypeAny], ['calendar', CalendarConfigSchema as unknown as z.ZodTypeAny], ['map', ListMapConfigSchema as unknown as z.ZodTypeAny], ['tree', TreeConfigSchema as unknown as z.ZodTypeAny], ] as const; - for (const [label, schema] of NON_GRID_FOUR) { + for (const [label, schema] of PER_KIND) { const result = schema.safeParse({ limit: 10 }); expect(result.success, label).toBe(false); // Asserted on the REFUSED KEY LIST rather than on a stringified issue: // when the key is accepted there is no issue to stringify, and the red // then reads as an argument-type complaint instead of as a statement - // about this view type. Measured — it is how this case first reddened. + // about this view type. const refused = ((result as { error?: z.ZodError }).error?.issues ?? []) .filter((issue) => issue.code === 'unrecognized_keys') .flatMap((issue) => (issue as unknown as { keys?: string[] }).keys ?? []); - expect(refused, `${label} accepts an authorable row ceiling it should not declare`) + expect(refused, `${label} accepts a per-kind row ceiling it should not declare`) .toContain('limit'); } }); diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 0e1655baec2..2f2db5b9cce 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -1101,7 +1101,11 @@ export const PaginationConfigSchema = lazySchema(() => strictObject({ surface: 'this pagination configuration', history: VIEW_HISTORY, }, { - pageSize: z.number().int().positive().default(25).describe('Number of records per page'), + pageSize: z.number().int().positive().default(25).describe( + 'Number of records per page. On a view with no pager (kanban, gallery, timeline) it is the fetch ' + + 'ceiling, and the renderer owes two things: bound its fetch at this number, and, when the filtered ' + + 'set is larger than it, show a visible truncation signal saying what is on screen is not the whole set', + ), pageSizeOptions: z.array(z.number().int().positive()).optional().describe('Available page size options'), })); @@ -1365,210 +1369,10 @@ export const GroupingConfigSchema = lazySchema(() => strictObject({ + 'AND-ed into the view filter. Compiled by `compileListViewGroupQuery` / `compileListViewGroupRowsQuery`', )); -/* - * --------------------------------------------------------------------------- - * `limit` — the author-settable row ceiling of the page-shaped views (#17393) - * --------------------------------------------------------------------------- - * - * Declared here because the PROTOCOL was the thing that was wrong: two - * renderers already cap by author choice, and the key they read was never a - * protocol key. Measured in objectui at `dda8f3815d`: - * - * - `ObjectKanban.tsx:573` fetches `$top: schema.limit ?? DEFAULT_KANBAN_LIMIT` - * (`= 100` at `:84`), and that `limit` is declared in `@object-ui/types` - * alone (`zod/objectql.zod.ts:1762`, `z.number().int().positive().optional()`); - * - `ObjectTimeline.tsx:328` fetches `$top: schema.limit ?? DEFAULT_TIMELINE_LIMIT` - * (`= 100` at `:29`), with `limit` on that component's own props interface - * (`:129`) and on no published schema at all; - * - `ObjectGallery.tsx` sends no `$top` and reads no ceiling at all — the - * unbounded fetch objectui#7390 is ruled to close by reading this key. - * - * Consumer-local author-settable keys the protocol never declared are the - * divergence the contract-first directive forbids, so the knob enters the - * protocol first and the three spellings come under one declaration (the - * director seat's amendment of 2026-09-10T11:0xZ on objectui#7390, on the - * maintainer's principle 「我们的项目以objectstack 协议为准,文档应该以实际实现 - * 为准。协议不正确的应该先修改协议。」). - * - * ## Why on the per-view config blocks, and not as a member of the list view - * - * The alternative shape — one row ceiling on {@link ListViewShapeSchema} - * itself — is rejected on three properties of this tree: - * - * 1. The base shape ALREADY carries the row-bounding knob every view type - * reaches: `pagination.pageSize` ({@link PaginationConfigSchema}, default - * 25). A second base-level row key would leave one view with two - * base-level row bounds and no declared precedence between them — and the - * `virtualScroll` tombstone at the bottom of this same shape prescribes - * `pagination` for exactly that question. - * 2. A base member is reachable from EVERY `type`, the non-grid four - * (gantt / calendar / map / tree) included. Their ceiling is a platform - * constant the renderer owns (objectui#7210) and this card does not touch - * them, so a base member would publish an authorable ceiling on four view - * kinds no renderer reads — declared-but-unenforced on the day it lands. - * 3. The per-kind block is what actually REACHES the renderer: objectui's - * `ListView` merges `schema.` into the generated node — its kanban - * branch spreads the rest of the block flat onto `object-kanban`, so - * `kanban.limit` lands exactly where `schema.limit` is read — while a - * base-level key is forwarded into no per-kind node at all. - * - * The NAME is `limit` for the same reason: it is the name the consumer already - * reads, so this declaration absorbs the two consumer-local keys instead of - * buying a second divergence spelled differently. - * - * ⚠️ NOT the kanban LANE's `limit`. objectui's node-level - * `ObjectKanbanLaneSchema.limit` is a WIP warning threshold that never reaches - * a query; no lane object exists on this face at all - * ({@link KanbanConfigSchema}'s `columns` is a list of card FIELD names), so - * the two cannot be confused here. - */ -export const DEFAULT_VIEW_ROW_LIMIT = 100; - -/** What a page-shaped view's ceiling bounds, per view type. */ -const ROW_LIMIT_SUBJECT = { - gallery: 'cards the gallery fetches and draws', - kanban: 'records the board fetches across all its lanes', - timeline: 'rows the timeline fetches onto its rail', -} as const; - -/** The three view configs that cap by AUTHOR choice (not by platform ceiling). */ -type RowLimitView = keyof typeof ROW_LIMIT_SUBJECT; - -/** - * The `limit` declaration for one page-shaped view config. - * - * The default is APPLIED, not merely described: a `.describe()` naming a - * default the schema does not apply is a second contract that nothing - * enforces, and the two drift the first time either is edited. The agreement - * is pinned from both sides in `view.test.ts` (#17393) — the parsed default is - * compared against the number the describe text states. - * - * ⛔ The truncation signal is the renderer's half and cannot be enforced from - * here; it is stated in the describe because a bounded-and-silent view reads - * as complete, which is worse than the unbounded-and-silent one this key - * replaces — the author needs to know the cap is visible, and the renderer - * author needs to know it is owed. - * - * ⚠️ WHICH FACE THIS KEY IS ON, and why that has to be said first. There are - * TWO `limit`s a reader can confuse, on two different documents, and three - * rounds of #19228 went wrong on the boundary: - * · **VIEW FACE** — THIS key. A member of a `ListViewSchema` document's - * `kanban` / `gallery` / `timeline` block. An ADAPTER turns that document - * into a rendered node; no renderer reads this document directly. - * · **ELEMENT FACE** — a page component node's OWN `limit` - * (`ObjectKanbanPropsSchema`, `ObjectTimelinePropsSchema`), - * declared in `component.zod.ts`, with no applied default. That is the key - * every renderer and `ElementDataSourceGate` actually read. - * Every sentence below names its face before it says anything else. - * - * ⚠️ WHAT THIS VIEW-FACE KEY REACHES TODAY — recorded, not repaired (#19228). - * Measured first-hand at the pin this repo builds against (`.objectui-sha` = - * `f8a9d0fb0`), with TWO instruments, because one was not enough and the - * first one's answer was wrong. First taken at `87af769e9` 2026-09-21T09:15Z; - * re-taken at `62597c588` 2026-09-23, where instrument 1 returned the SAME - * hit set line for line (probe 0; control 13 lines, 6 files) and all nine - * objectui files cited below were byte-identical to `87af769e9`; re-taken - * again at `f8a9d0fb0` 2026-09-24, where the probe is still 0, the control - * gains ONE line in a seventh file (a test, below), instrument 2 returns the - * same four flattening spreads and the same exclusions, and every anchor in - * the six cited files that changed (both `ObjectView.tsx`, `ListView.tsx`, - * `ObjectKanban.tsx`, `ObjectGallery.tsx`; `ObjectTimeline.tsx` and the - * three cited tests are byte-identical) MOVED with its cited text - * byte-identical — so both verdicts stand unmoved, at the numbers below: - * - * 1. PROPERTY-ACCESS spellings. ⛔ Published as its EXPRESSION, not as a - * number — this card exists because a confident count was wrong once, so - * a control nobody can re-derive is not a control. Run at the pin, from - * an objectui checkout, over every tracked file: - * probe: git grep -nIE '\.(kanban|gallery|timeline)(\?)?\.limit\b' - * control: git grep -nIE '\.(kanban|gallery|timeline)(\?)?\.(groupByField|scale|coverField)\b' - * Probe: **0** lines, 0 files. Control: **14** lines across **7** files — - * `app-shell/src/views/ObjectView.galleryBinding-7547.test.tsx:41`, - * `app-shell/src/views/ObjectView.tsx:451`, - * `plugin-detail/src/__tests__/RelatedList.selectFls-10186.test.tsx:314` - * (new at this pin, an executable test line), - * `plugin-list/src/ListView.tsx:2551`, `:2553`, `:2560`, `:3145`, `:3202`, - * `:3204`, - * `plugin-list/src/__tests__/ListView.kanbanOptionsBagCanonical-8193.test.tsx:42`, - * `:99`, `plugin-view/src/ObjectView.tsx:1723`, and - * `types/src/__tests__/object-kanban-group-by-limit-7322.test.ts:146`, `:148`. - * ⚠️ Filtering changes that number and the filter must be stated with it. - * Of the 14: **2 are COMMENTS** (`ObjectView.galleryBinding-7547.test.tsx:41`, - * `ListView.kanbanOptionsBagCanonical-8193.test.tsx:42`), **1 is an - * `it()` TITLE string** (same file, `:99` — ⛔ not a comment), and **2 are - * lines inside a QUOTED source-text pin** - * (`object-kanban-group-by-limit-7322.test.ts:146`, `:148`). So a reader - * counting executable reads only gets **9** (8 at `62597c588`). All three readings are of one - * hit set. A live instrument — and a WRONG answer. - * 2. ⭐ SPREADS — a spread carries a key without ever spelling it, so it is - * the hole instrument 1 cannot see by construction. ⛔ Re-take it by its - * PREDICATE, not by its count: **a spread whose target is the object - * literal an adapter RETURNS as the node** — flattening onto the node — - * as against a merge that builds a nested config (`...mergedTimeline` is - * the lit control for the instrument AND the example of what the predicate - * excludes). A grep broad enough to find these also returns the nested - * merges, so the rule, not the number, is what makes it reproducible. - * ⛔ And name what the predicate EXCLUDES, or the next reader re-finds - * it and wonders: `app-shell/src/views/ObjectView.tsx:207` and `:343` - * ARE spreads of a view block, inside `timelineViewOptions` (`:202`) and - * `galleryViewOptions` (`:335`). They build an OPTIONS BAG that feeds - * `ListView`'s nested forward, not the object literal an adapter returns - * as the node, so the predicate excludes them — deliberately, not by - * oversight. Two more the predicate excludes for their own reasons: - * `plugin-list/src/ListView.tsx:3132-3134` (`mergedGallery`) builds a - * NESTED gallery prop, the `...mergedTimeline` family; and - * `app-shell/src/views/ObjectView.tsx:1406` - * (`spec.kanban = { ...(spec.kanban || {}), columns }`) writes back into a - * VIEW document's own block — a metadata write, not a node build. - * Under that predicate, at that pin, the VIEW-face per-kind blocks give: - * `plugin-list/src/ListView.tsx:3067` `...restKanban` - * `plugin-view/src/ObjectView.tsx:1666` `...restKanban` - * `plugin-view/src/ObjectView.tsx:1725` `...(viewOptions.gallery || {})` - * `plugin-view/src/ObjectView.tsx:1753` `...(viewOptions.timeline || {})` - * Neither `restKanban` destructure strips `limit` (`ListView.tsx:3040`, - * `ObjectView.tsx:1607`), so a VIEW's per-kind `limit` — INCLUDING the 100 - * this applied default materializes — becomes the generated node's - * ELEMENT-face flat `limit`, which is the key the renderers read. - * - * ⇒ **A view's `kanban.limit`: flattened on BOTH adapter routes, and read.** - * `ObjectKanban.tsx:554` runs `describeRefusedRowLimit(schema.limit, …)` - * unconditionally. - * ⇒ **A view's `timeline.limit`: ROUTE-DEPENDENT.** `plugin-view` flattens it - * (`ObjectView.tsx:1753`) and the node it returns carries no `timeline` - * block at all, so the value arrives as the node's flat `limit` and - * `ObjectTimeline.tsx:279` reads it. `plugin-list` instead forwards the - * block NESTED (`ListView.tsx:3172`), where nothing reads it. - * ⇒ **A view's `gallery.limit`: flattened by `ObjectView.tsx:1725` and read by - * NOBODY** — `ObjectGallery.tsx` contains no `limit` at all (0 occurrences, - * case-insensitive, against a lit control `schema.imageField` / - * `schema.titleField` at `:340` / `:348`). ⛔ Do not generalise that - * asymmetry to the other two; it is gallery's alone. - * - * ⚠️ Where it IS read, the `$top` it would govern (`ObjectKanban.tsx:687`, - * `ObjectTimeline.tsx:407`) is still not issued on either adapter route today: - * both hosts hand rows down as a React `data` prop (`ListView.tsx:4815`, - * `ObjectView.tsx:2347`) and both children short-circuit their own fetch on it - * (`ObjectKanban.tsx:565`, `ObjectTimeline.tsx:420`). ⛔ That is a statement - * about the QUERY, not about the key being unread. - * - * ⚠️ A consequence of APPLIED that the open decision needs: through those - * spreads a spec-parsed view emits a node carrying an authored-LOOKING - * ELEMENT-face `limit: 100` that no author wrote. ⛔ Flagged, not acted on — - * changing it is a contract direction, not a tidy-up. - * - * ⛔ Which of the row bounds wins is NOT decided here and NOT implied by this - * declaration: #19228 opens that question and picks nothing, and neither does - * this note. What is recorded is only what each key reaches today. - */ -const rowLimitKey = (view: RowLimitView) => - z.number().int().positive().default(DEFAULT_VIEW_ROW_LIMIT).describe( - `Row ceiling — the most ${ROW_LIMIT_SUBJECT[view]}; default ` - + `${DEFAULT_VIEW_ROW_LIMIT} when the key is absent. The renderer owes two things: bound its ` - + 'fetch at this number, and, when the ceiling APPLIES (the filtered set is larger than it), ' - + 'show a visible truncation signal saying what is on screen is not the whole set — a bounded ' - + 'view that looks complete is worse than an unbounded one. ⚠️ Not every view kind has a ' - + 'renderer that reads this key yet; which do is recorded on the declaration.', - ); +/** The per-kind blocks answer `limit` with this; `timeline` is also nested on `object-timeline`. */ +const VIEW_ROW_BOUND_GUIDANCE = + 'This block declares no `limit`. Delete the key: the row bound is `pagination.pageSize` on a view, ' + + 'or the flat `limit` on a page component node.'; /** * Gallery View Configuration (Airtable-style) @@ -1577,13 +1381,13 @@ const rowLimitKey = (view: RowLimitView) => export const GalleryConfigSchema = lazySchema(() => strictObject({ surface: 'this gallery configuration', history: VIEW_HISTORY, + guidance: { limit: VIEW_ROW_BOUND_GUIDANCE }, }, { coverField: z.string().optional().describe('Attachment/image field to display as card cover'), coverFit: z.enum(['cover', 'contain']).default('cover').describe('Image fit mode for card cover'), cardSize: z.enum(['small', 'medium', 'large']).default('medium').describe('Card size in gallery view'), titleField: z.string().optional().describe('Field to display as card title'), visibleFields: z.array(z.string()).optional().describe('Fields to display on card body'), - limit: rowLimitKey('gallery'), }).describe('Gallery/card view configuration')); /** @@ -1593,6 +1397,7 @@ export const GalleryConfigSchema = lazySchema(() => strictObject({ export const TimelineConfigSchema = lazySchema(() => strictObject({ surface: 'this timeline configuration', history: VIEW_HISTORY, + guidance: { limit: VIEW_ROW_BOUND_GUIDANCE }, }, { startDateField: z.string().describe('Field for timeline item start date'), endDateField: z.string().optional().describe('Field for timeline item end date'), @@ -1606,7 +1411,6 @@ export const TimelineConfigSchema = lazySchema(() => strictObject({ ), colorField: z.string().optional().describe('Field to derive each item color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the timeline default marker color'), scale: z.enum(['hour', 'day', 'week', 'month', 'quarter', 'year']).default('week').describe('Default timeline scale'), - limit: rowLimitKey('timeline'), }).describe('Timeline view configuration')); /** @@ -1881,6 +1685,7 @@ export const AddRecordConfigSchema = lazySchema(() => strictObject({ export const KanbanConfigSchema = lazySchema(() => strictObject({ surface: 'this kanban configuration', history: VIEW_HISTORY, + guidance: { limit: VIEW_ROW_BOUND_GUIDANCE }, }, { groupByField: z.string() .superRefine(groupByFieldCheck('kanban')) @@ -1910,7 +1715,6 @@ export const KanbanConfigSchema = lazySchema(() => strictObject({ */ titleField: z.string().optional().describe('Field displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain)'), columns: z.array(z.string()).describe('Fields to show on cards'), - limit: rowLimitKey('kanban'), })); /** @@ -6585,8 +6389,6 @@ export type CalendarConfig = z.input; export type GanttConfig = z.input; export type GanttQuickFilter = z.input; export type KanbanConfig = z.input; -/** Post-parse shape of {@link KanbanConfig} — defaults applied, transforms run (ADR-0122). */ -export type KanbanConfigParsed = z.infer; export type ListMapConfig = z.input; export type NavigationMode = z.input; export type TreeConfig = z.input;