Skip to content

Commit de0bcdd

Browse files
huangyiireneclaude
andauthored
feat(spec): object-kanban props declare limit, the row cap objectui already implements (#16562)
* feat(spec): object-kanban props declare `limit`, the row cap objectui already reads `ComponentPropsMap['object-kanban']` is strict and refused `limit` by name while objectui's plugin-kanban reads `schema.limit` as the query's `$top`, `OBJECT_KANBAN_DATA_SOURCE` maps `limit: 'limit'`, `KanbanSchema` declares `limit?: number` and the plugin docs teach `limit: 250`. Declare the key (`z.number().int().positive().optional()`), pin both directions (the documented shape parses; an undeclared sibling is still refused), and add the minor changeset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno * chore(spec): regenerate authorable-surface and reference docs for object-kanban `limit` `pnpm --filter @objectstack/spec build` (gen:schema) adds the `ui/ObjectKanbanProps:limit` row to `authorable-surface/ui.json`; `check:generated` proved only `content/docs/references/**` stale and `gen:docs` regenerated it. No `api-surface/` shard moves — a key on an existing props map adds no export. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7092d63 commit de0bcdd

5 files changed

Lines changed: 119 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `ComponentPropsMap['object-kanban']` declares `limit`, the row cap four objectui faces already implement (#16503, the spec half of objectui#8172)
6+
7+
`object-kanban` gains one optional authorable key:
8+
9+
```ts
10+
limit: z.number().int().positive().optional()
11+
```
12+
13+
Maximum number of records loaded onto the board (row cap), lowered to the top-level `$top` of the board's one query. The renderer default stays 100 and is documented rather than declared, so an unset key remains unset. The component-level `dataSource.limit` wins when both are set, and a bound named view's `pagination.pageSize` fills the key only when the component authored none — the `ElementDataSourceGate` precedence table, unchanged.
14+
15+
Measured at the objectui pin this repo builds against (`.objectui-sha` = `a472b0716`): `plugin-kanban` reads `schema.limit` as the query's `$top` (wired by objectui#4025), `OBJECT_KANBAN_DATA_SOURCE` maps `limit: 'limit'`, `KanbanSchema` declares `limit?: number`, and `content/docs/plugins/plugin-kanban.mdx` teaches it with a typed snippet (`limit: 250`) plus a Properties row. The strict props map refused the key by name, so an author following the published docs wrote a node the save gate rejected with the same `unrecognized_keys` verdict a typo gets. Decision batch #68 (2026-09-07, option A): the contract declares the capability that is already implemented, documented and in use.
16+
17+
Widening a published accept set (Clause-② yes): `safeParse({ objectName: 'x', limit: 250 })` now succeeds; every other undeclared key on the node is refused exactly as before. objectui#8172 publishes the key in the registry declaration on its side.

content/docs/references/ui/component.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ Sort field and direction pair
457457
| **groupBy** | `string` | optional | Field whose values become the board columns |
458458
| **columns** | `any[]` | optional | Swimlane definitions (`{ id, title }` per `groupBy` value, or bare value strings) — NOT a field projection |
459459
| **filter** | `any` | optional | Base query filter, handed to the wire `$filter` |
460+
| **limit** | `integer` | optional | Maximum number of records loaded onto the board (row cap); lowered to the query's top-level `$top` (renderer default 100). The component-level `dataSource.limit` wins when both are set; a bound view's `pagination.pageSize` fills it only when unset |
460461
| **data** | `any[]` | optional | Static inline cards — bypasses the object query |
461462
| **cardTitle** | `string` | optional | Field rendered as each card title |
462463
| **titleField** | `string` | optional | Legacy fallback for `cardTitle` (the board reads `cardTitle \|\| titleField`). Prefer `cardTitle` |

packages/spec/authorable-surface/ui.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,7 @@
778778
"ui/ObjectKanbanProps:filter",
779779
"ui/ObjectKanbanProps:groupBy",
780780
"ui/ObjectKanbanProps:grouping",
781+
"ui/ObjectKanbanProps:limit",
781782
"ui/ObjectKanbanProps:objectName",
782783
"ui/ObjectKanbanProps:quickAdd",
783784
"ui/ObjectKanbanProps:swimlaneField",

packages/spec/src/ui/component.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
ElementRecordPickerPropsSchema,
2222
ElementTextInputPropsSchema,
2323
ObjectMetricPropsSchema,
24+
ObjectKanbanPropsSchema,
2425
} from './component.zod';
2526
import { PageComponentSchema, PageSchema, PageComponentType, ElementDataSourceSchema } from './page.zod';
2627

@@ -2772,6 +2773,68 @@ describe('#7751 — object-* block props schemas', () => {
27722773
});
27732774
});
27742775

2776+
// #16503 — the spec half of objectui#8172 (decision batch #68, 2026-09-07,
2777+
// option A: the contract declares the capability that already ships, is
2778+
// documented and is in use). Measured at the objectui pin this repo builds
2779+
// against (`.objectui-sha` = `a472b0716`): `plugin-kanban/src/ObjectKanban.tsx:264`
2780+
// queries `$top: schema.limit ?? DEFAULT_KANBAN_LIMIT` (100, `:71`),
2781+
// `plugin-kanban/src/index.tsx:395-398` maps `limit: 'limit'` in
2782+
// `OBJECT_KANBAN_DATA_SOURCE`, `plugin-kanban/src/types.ts:134` declares
2783+
// `KanbanSchema.limit?: number`, and `content/docs/plugins/plugin-kanban.mdx`
2784+
// teaches `limit: 250` with a Properties row. The strict map refused the key by
2785+
// name — the same `unrecognized_keys` verdict as the `bogusProp` control — so an
2786+
// author following the published docs wrote a node the save gate rejected.
2787+
describe('ObjectKanbanPropsSchema limit — the row cap four objectui faces already implement (#16503)', () => {
2788+
const kanban = ComponentPropsMap['object-kanban'];
2789+
2790+
it("accepts the documented shape `{ objectName: 'x', limit: 250 }` and carries the value through", () => {
2791+
const result = kanban.safeParse({ objectName: 'x', limit: 250 });
2792+
expect(result.success).toBe(true);
2793+
const parsed = (result.success ? result.data : undefined) as { limit?: number } | undefined;
2794+
// Carried through to the parsed output, not stripped: what the board
2795+
// lowers to `$top` is what the author wrote.
2796+
expect(parsed?.limit).toBe(250);
2797+
});
2798+
2799+
it('still refuses an undeclared sibling on the same node — the accept above is not vacuous', () => {
2800+
// The card's own control, and the half that proves the object stayed
2801+
// strict: without it the green above would also be green on a map that
2802+
// had stopped refusing anything.
2803+
const result = kanban.safeParse({ objectName: 'x', bogusProp: 250 });
2804+
expect(result.success).toBe(false);
2805+
const issue = result.error?.issues.find((i) => i.code === 'unrecognized_keys') as
2806+
| { keys?: string[] }
2807+
| undefined;
2808+
expect(issue?.keys).toEqual(['bogusProp']);
2809+
});
2810+
2811+
it('refuses a cap the query could not lower to `$top` — zero, negative, fractional, or a string — at the VALUE, not the key', () => {
2812+
// `z.number().int().positive()`: the shape `element:record_picker` and
2813+
// `record:related_list` declare for the same `$top` read, so the flat row
2814+
// caps in this map are one contract rather than three dialects. The key is
2815+
// recognised (no `unrecognized_keys`); the value is what fails.
2816+
for (const limit of [0, -1, 1.5, '250']) {
2817+
const result = kanban.safeParse({ objectName: 'x', limit });
2818+
expect(result.success, JSON.stringify(limit)).toBe(false);
2819+
const codes = (result.error?.issues ?? []).map((i) => i.code);
2820+
expect(codes, JSON.stringify(limit)).not.toContain('unrecognized_keys');
2821+
expect(result.error?.issues[0]?.path, JSON.stringify(limit)).toEqual(['limit']);
2822+
}
2823+
});
2824+
2825+
it('keeps a `.describe()` that names the `$top` the board lowers it to and the binding that outranks it', () => {
2826+
// The describe is the artifact an auditor reads instead of hunting across
2827+
// repos, and the row the generated reference page prints; deleting it is
2828+
// what re-opens the "is this key live?" question this record answers.
2829+
const shape = (ObjectKanbanPropsSchema as unknown as {
2830+
def: { shape: Record<string, { description?: string }> };
2831+
}).def.shape;
2832+
expect(shape.limit?.description).toContain('$top');
2833+
expect(shape.limit?.description).toContain('row cap');
2834+
expect(shape.limit?.description).toContain('dataSource.limit');
2835+
});
2836+
});
2837+
27752838
// #10053 — the accept-pins for the last two `icon` slots in this file whose
27762839
// describes stated only the VOCABULARY. "Icon name (Lucide)" is equally true of
27772840
// the `page:header` `icon` retired in #6946 *because nothing reads it*, so the

packages/spec/src/ui/component.zod.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2644,6 +2644,9 @@ export type ObjectMetricProps = z.input<typeof ObjectMetricPropsSchema>;
26442644
* forwarded schema `quickAdd`/`coverImageField`/`conditionalFormatting`
26452645
* (`KanbanRenderer`, index.tsx). `groupField` is the DESIGNER's spelling with
26462646
* zero read points (#7973 class) — aliased to the `groupBy` the board reads.
2647+
* `limit` (#16503) was measured later, at the pin this repo builds against
2648+
* (`.objectui-sha` = `a472b0716`): `ObjectKanban.tsx:264`, the `$top` of the
2649+
* board's one query — its docblock below carries the four-face record.
26472650
*/
26482651
export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({
26492652
surface: 'this `object-kanban`',
@@ -2662,6 +2665,40 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({
26622665
columns: z.array(z.unknown()).optional()
26632666
.describe('Swimlane definitions ({ id, title } per `groupBy` value, or bare value strings) — NOT a field projection'),
26642667
filter: z.unknown().optional().describe('Base query filter, handed to the wire `$filter`'),
2668+
/**
2669+
* Row cap (#16503 — the spec half of objectui#8172; decision batch #68,
2670+
* 2026-09-07, option A: the contract declares the capability that already
2671+
* ships, is documented and is in use). Measured at the objectui pin this
2672+
* repo builds against (`.objectui-sha` = `a472b0716`), four faces agreed
2673+
* while this map refused the key by name: the board's one query is
2674+
* `dataSource.find(objectName, { $filter: schema.filter, $top: schema.limit
2675+
* ?? DEFAULT_KANBAN_LIMIT })` (`plugin-kanban/src/ObjectKanban.tsx:262-266`,
2676+
* the default `100` at `:71` — a REAL top-level `$top` since objectui#4025;
2677+
* before that the cap sat under a `options` key no adapter read),
2678+
* `OBJECT_KANBAN_DATA_SOURCE` maps `limit: 'limit'`
2679+
* (`plugin-kanban/src/index.tsx:395-398`), `KanbanSchema` — the type
2680+
* `ObjectKanban.tsx:143` reads `schema` through — declares `limit?: number`
2681+
* (`plugin-kanban/src/types.ts:134`), and `content/docs/plugins/plugin-kanban.mdx`
2682+
* teaches it with a typed snippet (`limit: 250`) plus a Properties row. So
2683+
* an author following the published docs wrote a node the save gate
2684+
* refused, with the same `unrecognized_keys` verdict a typo gets.
2685+
*
2686+
* Why the carrier is `limit` and not the bound view's `pagination.pageSize`
2687+
* (the alternative the card opened): precedence is the `ElementDataSourceGate`
2688+
* table, not this key's. The component-level `dataSource.limit` overrides
2689+
* this key, and a bound named view's `pagination.pageSize` is LOWERED INTO
2690+
* it through the `limit: 'limit'` mapping only when the component authored
2691+
* none (`react/src/element-data-source/ElementDataSourceGate.tsx:236-241`,
2692+
* `readLimit`/`writeLimit` keyed by `ElementDataSourceLimitKey`). The board
2693+
* has no `pagination` read point, so declaring that spelling here would name
2694+
* a key the renderer ignores — the accepted-and-dropped defect this section
2695+
* exists to remove. Same shape as the `element:record_picker` and
2696+
* `record:related_list` row caps (one `$top` contract, not a third dialect),
2697+
* and like them the renderer's 100 is documented rather than declared:
2698+
* a schema default would materialize `limit: 100` on every parsed board.
2699+
*/
2700+
limit: z.number().int().positive().optional()
2701+
.describe("Maximum number of records loaded onto the board (row cap); lowered to the query's top-level `$top` (renderer default 100). The component-level `dataSource.limit` wins when both are set; a bound view's `pagination.pageSize` fills it only when unset"),
26652702
data: z.array(z.unknown()).optional().describe('Static inline cards — bypasses the object query'),
26662703
cardTitle: z.string().optional().describe('Field rendered as each card title'),
26672704
titleField: z.string().optional().describe('Legacy fallback for `cardTitle` (the board reads `cardTitle || titleField`). Prefer `cardTitle`'),

0 commit comments

Comments
 (0)