Skip to content

Commit 0a168a3

Browse files
feat(spec)!: FormSectionSchema.columns drops default(1) — undeclared section density is the renderer's, declared is the author's ceiling (#23771)
Fixes #23740 Clause-②: no ## What `FormSectionSchema.columns` (`packages/spec/src/ui/view.zod.ts`) drops `.default(1)`, per the maintainer ruling recorded on objectstack-ai/objectui#12521 (comment 6204264665): 「不用决策卡,我同意 b) 改协议(推荐): 去掉分节 columns 的 default(1)」. - The key stays optional with values `1`–`4`; the string spellings `'1'`–`'4'` keep their transform (now `parseInt(val, 10)`), and `undefined` passes through. Measured with zod 4.6.5: an absent key parses to an object with no `columns` key at all (not `columns: undefined`). - The schema gains a docblock and a `.describe()` stating the two meanings: **declared** = the author's ceiling (at most that many fields per row where the container is wide enough; collapsed by the renderer on narrow containers); **undeclared** = the renderer's choice by available width. No pixel or breakpoint value enters the protocol. - `FormViewSchema.columns` is untouched: its describe does not speak to sections. ## Files - `packages/spec/src/ui/view.zod.ts`: the schema change. - `packages/spec/scripts/lib/default-changes.ts`: a new `18: [...]` row declaring `ui/FormSection:columns: 1 → (none)`. The spec build's default ratchet refused the change until it was declared; its `reason` explains what moves for a consumer and why no `semantic` migration entry comes with it. A conversion that writes `columns: 1` onto stored sections would bring back the reading the ruling removed. - Generated: `packages/spec/authorable-defaults/ui.json` (the `ui/FormSection:columns = 1` line leaves) and `content/docs/references/ui/view.mdx` (three tables lose `default: 1` and gain the describe). `check:generated` named only `check:docs` as stale; `--fix` regenerated it. - Docs: `content/docs/protocol/objectui/layout-dsl.mdx` gets one paragraph on the two meanings. - Tests: the pins that asserted a parsed section's `columns: 1` now assert the key is absent. None was deleted. - `packages/spec/src/ui/view.test.ts` (plus a new case: declared `1`/`'2'`/`4` survive, and `5`/`'0'` are refused) - `packages/spec/src/ui/view-union-diagnostics.test.ts` (the `SECTION` parse output) - `packages/spec/src/ui/view-overlay-viewkind-arm.test.ts` (byte-identical form overlay output) - `packages/metadata-protocol/src/protocol.graft-folded-form-sections.test.ts` - `.changeset/23740-form-section-columns-no-default.md`: `@objectstack/spec` `minor`, with a BREAKING line, a before/after table of parse outputs, and the ADR-0087 disposition `not-required (no-migration-prescription)`. `check:adr-0087-registration` accepts that spelling (measured, exit 0). ## Evidence - Readers of a form section's `columns` in `packages/*/src` outside `view.zod.ts`: 0, re-measured at `2906cd8d2`. `protocol.ts` hand-builds a section with an explicit `columns: 2`, so it is unaffected. - `pnpm --filter @objectstack/spec build`: exit 0, after the default row was declared. - `pnpm --filter @objectstack/spec test`: 678 files passed, 1 skipped; 20755 tests passed. - `pnpm --filter @objectstack/spec typecheck`: exit 0. - `@objectstack/metadata-protocol`: its dependency closure was built, `protocol.graft-folded-form-sections.test.ts` passed 46 of 46, and `typecheck` exited 0. - Ablation, run once: `node scripts/ablation-replace.mjs` put `.default(1)` back on disk (anchor 1 → 0, blob `3dba72ea` → `f04fd827`). `view.test.ts` then failed 1 test, `FormSectionSchema > should accept minimal form section`. The tool restored the file: blob back to the HEAD blob `3dba72ea`, and `git diff HEAD` empty. - Derived gates (`dispatch-gates --commands`, at head `741552c0`): 110 derived, 109 ran with exit 0. `--ran` reconciles 110 of 110. - Two gates first exited 3 on prerequisites and passed after the prerequisite was fixed: `check-plugin-teardown-shape --self-test` (the shallow clone lacked its pinned fixture commit, which was fetched) and `check:skill-examples` (the client-react closure was built first). - **NOT MEASURED:** `check:dual-build-cjs-loads`, exit 3, PREREQUISITE NOT MET. It needs every package's `dist/`, which takes a repo-wide build. Left to CI. ## Acceptance notes - objectui consumers named on the card (`metadata-admin/SchemaForm.tsx` reads `s.columns ?? 1`; plugin-form's sectioned layout) are objectui#12521 / objectui PR #12546's side. No objectui file is touched here. - `main` gained 3 commits since the branch point (pm-dispatch, plugin-security tests, objectui pin). None touches `packages/spec` or `metadata-protocol`, so no merge was made before opening. - This PR is meant to ride the same spec release as #23747. --- _Generated by [Claude Code](https://claude.ai/code/session_012vwv8sA1ZBBjmphdHWo9i5)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ec44cf6 commit 0a168a3

10 files changed

Lines changed: 97 additions & 12 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: a form section's `columns` has no default — undeclared, the density is the renderer's (by available width); declared, it is the author's ceiling
6+
7+
Clause-②: no
8+
9+
**BREAKING:** a parsed form section without `columns` no longer carries `columns: 1`. `FormSectionSchema.columns` drops `.default(1)`: the key stays optional with values `1`–`4`, and the string spellings `'1'`–`'4'` still parse to the number.
10+
11+
| A form section authored as | Parsed before | Parsed now |
12+
|:---|:---|:---|
13+
| `{ fields: [...] }` (no `columns`) | `columns: 1` | no `columns` key |
14+
| `{ columns: 2, fields: [...] }` | `columns: 2` | `columns: 2` (unchanged) |
15+
| `{ columns: '3', fields: [...] }` | `columns: 3` | `columns: 3` (unchanged) |
16+
17+
The two meanings, stated on the schema's describe and docblock:
18+
19+
- **Declared** — the author's ceiling: at most that many fields per row where the container is wide enough. A renderer collapses it on narrow containers.
20+
- **Undeclared** — the renderer's choice, by available width. After parse a renderer can now tell "the author said nothing" apart from "the author asked for one column", which the old default erased. No pixel or breakpoint value enters the protocol.
21+
22+
Nothing to rewrite: every input accepted before is accepted now with the same declared value, and none refused before is accepted. A section that must stay single-column on a wide container declares `columns: 1`. A consumer that reads a parsed section's `columns` treats an absent key as "the renderer decides" rather than as `1`. `FormViewSchema.columns` (the form body) was already optional with no default and is unchanged.
23+
24+
<!-- adr-0087: not-required (no-migration-prescription) no key is removed, renamed or moved and no stored or authored value changes meaning when declared; only the parse-added default for an undeclared key goes away, which no conversion can restore without defeating the change -->

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,12 @@ regions:
242242
are written in the vocabulary the schemas accept.
243243
</Callout>
244244

245+
A section's `columns` has no default. Declared, it is the author's **ceiling**:
246+
at most that many fields per row where the container is wide enough, collapsed
247+
by the renderer on narrow containers — so `columns: 1` is how to keep a section
248+
single-column. Undeclared, the density is the **renderer's** choice by available
249+
width. No pixel or breakpoint value is part of the protocol.
250+
245251
All layouts use a responsive grid that divides space into columns.
246252

247253
### Basic Grid Layout

‎content/docs/references/ui/view.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ Form-view select option — the object-field option shape minus the per-option `
322322
| **collapsed** | `boolean` | optional (default: `false`) | Whether the section starts closed. Default `false`. ⚠️ `collapsed: true` IMPLIES `collapsible` and is sufficient ON ITS OWN — a section that starts closed always carries the disclosure control that reopens it, and it outranks an explicit `collapsible: false`, so writing `collapsed: true` alone is a correct way to say "collapsed by default". The implication is a renderer rule, never a parse-time rewrite: `{ collapsed: true }` still parses to `collapsible: false, collapsed: true`, so the parsed `collapsible` must never be read as "a control renders". Only `true` is refused on a wizard step (steps do not collapse) and beside `group`, whose field group declares the pair. |
323323
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — section shown only when TRUE. Root: `record` (+ `previous`, `parent`) in runtime forms, or `data` in metadata forms. `current_user` (and the ADR-0068 aliases `user` / `ctx.user` / `os.user`) resolves here too — CLIENT-SIDE only: nothing server-side evaluates a form-view section `visibleWhen`, so a role test here hides the controls and protects no data (declare permission-set field-level security for that), and on the public `/f/:slug` route no host publishes a scope, so the root is unbound and the predicate faults open. No `features.*` on ANY form-view predicate — refused at parse: unbound on the standalone form routes, where the predicate would fault open. |
324324
| **visibleOn** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Hides the whole section when false. Normalized to `visibleWhen` at parse. |
325-
| **columns** | `Enum<'1' \| '2' \| '3' \| '4'> \| 1 \| 2 \| 3 \| 4` | optional (default: `1`) | |
325+
| **columns** | `Enum<'1' \| '2' \| '3' \| '4'> \| 1 \| 2 \| 3 \| 4` | optional | Fields per row, 1–4 ('1'–'4' parse to the number). Declared: the author's ceiling — at most that many per row where the container is wide enough; a renderer collapses it on narrow containers. Undeclared: the renderer's choice by available width — the parse adds no default, so declare `columns: 1` to keep a section single-column. |
326326
| **pane** | `Enum<'primary' \| 'secondary'>` | optional | Split pane this section renders in (split forms only; a parse error elsewhere). Omitted → first section 'primary', others 'secondary'. |
327327
| **group** | `string` | optional | Field group key (snake_case) whose members and presentation this section inherits, from the bound object's `fieldGroups` (ADR-0085 §5 `deriveFieldGroupLayout`). Mutually exclusive with `fields`, and with every key the group itself declares (`name`, `label`, `description`, `collapsible`, `collapsed`, `visibleWhen`/`visibleOn`). Not valid on a wizard step. Must name a declared group — checked by reference diagnostics. |
328328
| **fields** | `(string \| { field: string; type?: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; options?: object[]; reference?: string; … })[]` | optional | |
@@ -446,7 +446,7 @@ Form-view select option — the object-field option shape minus the per-option `
446446
| **collapsed** | `boolean` | optional (default: `false`) | Whether the section starts closed. Default `false`. ⚠️ `collapsed: true` IMPLIES `collapsible` and is sufficient ON ITS OWN — a section that starts closed always carries the disclosure control that reopens it, and it outranks an explicit `collapsible: false`, so writing `collapsed: true` alone is a correct way to say "collapsed by default". The implication is a renderer rule, never a parse-time rewrite: `{ collapsed: true }` still parses to `collapsible: false, collapsed: true`, so the parsed `collapsible` must never be read as "a control renders". Only `true` is refused on a wizard step (steps do not collapse) and beside `group`, whose field group declares the pair. |
447447
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — section shown only when TRUE. Root: `record` (+ `previous`, `parent`) in runtime forms, or `data` in metadata forms. `current_user` (and the ADR-0068 aliases `user` / `ctx.user` / `os.user`) resolves here too — CLIENT-SIDE only: nothing server-side evaluates a form-view section `visibleWhen`, so a role test here hides the controls and protects no data (declare permission-set field-level security for that), and on the public `/f/:slug` route no host publishes a scope, so the root is unbound and the predicate faults open. No `features.*` on ANY form-view predicate — refused at parse: unbound on the standalone form routes, where the predicate would fault open. |
448448
| **visibleOn** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Hides the whole section when false. Normalized to `visibleWhen` at parse. |
449-
| **columns** | `Enum<'1' \| '2' \| '3' \| '4'> \| 1 \| 2 \| 3 \| 4` | optional (default: `1`) | |
449+
| **columns** | `Enum<'1' \| '2' \| '3' \| '4'> \| 1 \| 2 \| 3 \| 4` | optional | Fields per row, 1–4 ('1'–'4' parse to the number). Declared: the author's ceiling — at most that many per row where the container is wide enough; a renderer collapses it on narrow containers. Undeclared: the renderer's choice by available width — the parse adds no default, so declare `columns: 1` to keep a section single-column. |
450450
| **pane** | `Enum<'primary' \| 'secondary'>` | optional | Split pane this section renders in (split forms only; a parse error elsewhere). Omitted → first section 'primary', others 'secondary'. |
451451
| **group** | `string` | optional | Field group key (snake_case) whose members and presentation this section inherits, from the bound object's `fieldGroups` (ADR-0085 §5 `deriveFieldGroupLayout`). Mutually exclusive with `fields`, and with every key the group itself declares (`name`, `label`, `description`, `collapsible`, `collapsed`, `visibleWhen`/`visibleOn`). Not valid on a wizard step. Must name a declared group — checked by reference diagnostics. |
452452
| **fields** | `(string \| { field: string; type?: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; options?: object[]; reference?: string; … })[]` | optional | |
@@ -462,7 +462,7 @@ Form-view select option — the object-field option shape minus the per-option `
462462
| **collapsed** | `boolean` | optional (default: `false`) | Whether the section starts closed. Default `false`. ⚠️ `collapsed: true` IMPLIES `collapsible` and is sufficient ON ITS OWN — a section that starts closed always carries the disclosure control that reopens it, and it outranks an explicit `collapsible: false`, so writing `collapsed: true` alone is a correct way to say "collapsed by default". The implication is a renderer rule, never a parse-time rewrite: `{ collapsed: true }` still parses to `collapsible: false, collapsed: true`, so the parsed `collapsible` must never be read as "a control renders". Only `true` is refused on a wizard step (steps do not collapse) and beside `group`, whose field group declares the pair. |
463463
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — section shown only when TRUE. Root: `record` (+ `previous`, `parent`) in runtime forms, or `data` in metadata forms. `current_user` (and the ADR-0068 aliases `user` / `ctx.user` / `os.user`) resolves here too — CLIENT-SIDE only: nothing server-side evaluates a form-view section `visibleWhen`, so a role test here hides the controls and protects no data (declare permission-set field-level security for that), and on the public `/f/:slug` route no host publishes a scope, so the root is unbound and the predicate faults open. No `features.*` on ANY form-view predicate — refused at parse: unbound on the standalone form routes, where the predicate would fault open. |
464464
| **visibleOn** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Hides the whole section when false. Normalized to `visibleWhen` at parse. |
465-
| **columns** | `Enum<'1' \| '2' \| '3' \| '4'> \| 1 \| 2 \| 3 \| 4` | optional (default: `1`) | |
465+
| **columns** | `Enum<'1' \| '2' \| '3' \| '4'> \| 1 \| 2 \| 3 \| 4` | optional | Fields per row, 1–4 ('1'–'4' parse to the number). Declared: the author's ceiling — at most that many per row where the container is wide enough; a renderer collapses it on narrow containers. Undeclared: the renderer's choice by available width — the parse adds no default, so declare `columns: 1` to keep a section single-column. |
466466
| **pane** | `Enum<'primary' \| 'secondary'>` | optional | Split pane this section renders in (split forms only; a parse error elsewhere). Omitted → first section 'primary', others 'secondary'. |
467467
| **group** | `string` | optional | Field group key (snake_case) whose members and presentation this section inherits, from the bound object's `fieldGroups` (ADR-0085 §5 `deriveFieldGroupLayout`). Mutually exclusive with `fields`, and with every key the group itself declares (`name`, `label`, `description`, `collapsible`, `collapsed`, `visibleWhen`/`visibleOn`). Not valid on a wizard step. Must name a declared group — checked by reference diagnostics. |
468468
| **fields** | `(string \| { field: string; type?: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; options?: object[]; reference?: string; … })[]` | optional | |

‎packages/metadata-protocol/src/protocol.graft-folded-form-sections.test.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,12 +261,15 @@ describe('#7134 what the save path must NOT change', () => {
261261
it('[#20051] a form authored with canonical `sections` is stored as authored — no section default, no `sharing.enabled`, no undeclared key', async () => {
262262
// Re-judged at stage (iv): was "stored byte-identical", with an
263263
// `isPinned` the form member does not declare riding along. The parse
264-
// adds `collapsible` / `collapsed` / `columns` to the section and
265-
// `enabled: false` to `sharing`; ruling B stores none of them (ADR-0087's
266-
// `storable` rule), and the undeclared `isPinned` is dropped.
264+
// adds `collapsible` / `collapsed` to the section and `enabled: false`
265+
// to `sharing`; ruling B stores none of them (ADR-0087's `storable`
266+
// rule), and the undeclared `isPinned` is dropped. A section's `columns`
267+
// has no default: undeclared, the parse adds no key (the renderer picks
268+
// the density by available width).
267269
const authored = flatForm({ sections: [SECTION], isPinned: true });
268270
const parsed = (ViewMetadataSchema as any).parse(authored);
269-
expect(parsed.sections[0]).toMatchObject({ collapsible: false, collapsed: false, columns: 1 });
271+
expect(parsed.sections[0]).toMatchObject({ collapsible: false, collapsed: false });
272+
expect('columns' in parsed.sections[0]).toBe(false);
270273
expect(parsed.sharing.enabled).toBe(false);
271274
const body = await storedViewBody('contact_us', authored);
272275
const { isPinned: _dropped, ...declared } = authored as Record<string, unknown>;

‎packages/spec/authorable-defaults/ui.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
"ui/FormField:span = \"auto\"",
4646
"ui/FormSection:collapsed = false",
4747
"ui/FormSection:collapsible = false",
48-
"ui/FormSection:columns = 1",
4948
"ui/FormView:type = \"simple\"",
5049
"ui/GalleryConfig:cardSize = \"medium\"",
5150
"ui/GalleryConfig:coverFit = \"cover\"",

‎packages/spec/scripts/lib/default-changes.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,4 +620,27 @@ export const DEFAULT_CHANGES_BY_MAJOR: Readonly<Record<number, readonly Declared
620620
+ '`visibility: \'private\'` (on the CLI, `--visibility private`).',
621621
},
622622
],
623+
18: [
624+
{
625+
key: 'ui/FormSection:columns',
626+
from: '1',
627+
to: '(none)',
628+
reason:
629+
'A RULED declaration change (maintainer ruling record 6204264665: 「不用决策卡,我同意 b) '
630+
+ '改协议(推荐): 去掉分节 columns 的 default(1)」). A form section\'s `columns` now has two '
631+
+ 'meanings and no default: DECLARED, it is the author\'s ceiling — at most that many fields '
632+
+ 'per row where the container is wide enough, collapsed by the renderer on narrow '
633+
+ 'containers; UNDECLARED, the density is the renderer\'s choice by available width. The old '
634+
+ '`default(1)` materialised an authored-looking `columns: 1` onto every silent section, so '
635+
+ 'after parse a renderer could not tell "the author asked for one column" from "the author '
636+
+ 'said nothing" — on a wide dialog the second drew one long column, while the form body\'s '
637+
+ 'own `FormViewSchema.columns` was already optional with no default. What moves for a '
638+
+ 'consumer: a parsed section that omits `columns` no longer carries the key, so a reader of '
639+
+ 'the parsed value sees `1 | 2 | 3 | 4 | undefined`, where `undefined` means "the renderer '
640+
+ 'decides". To keep a section single-column on a wide container, write `columns: 1`. No '
641+
+ '`semantic` migration entry accompanies this: materialising `columns: 1` onto stored '
642+
+ 'sections would re-create exactly the reading the ruling removed, and every declared value '
643+
+ 'keeps its meaning.',
644+
},
645+
],
623646
};

‎packages/spec/src/ui/view-overlay-viewkind-arm.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ describe('controls — byte-identical parse output (measured on origin/main ce70
200200
});
201201
expect(branch).toBe('formOverlay');
202202
expect(JSON.stringify(data)).toBe(
203-
'{"type":"simple","sections":[{"label":"Main","collapsible":false,"collapsed":false,"columns":1,"fields":["name"]}],'
203+
// No `columns`: a section's undeclared density is the renderer's, and the parse adds no default.
204+
'{"type":"simple","sections":[{"label":"Main","collapsible":false,"collapsed":false,"fields":["name"]}],'
204205
+ '"name":"acct.f","object":"account","viewKind":"form"}',
205206
);
206207
});

‎packages/spec/src/ui/view-union-diagnostics.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,9 @@ describe('diagnoseViewMetadata names the failing branch', () => {
244244
// parse output re-measured, and the unbound original moves to REFUSED so the
245245
// flip itself is pinned rather than silently absorbed.
246246
describe('the acceptance face of ViewMetadataSchema — frozen by the diagnostics work; every move since is deliberate and pinned', () => {
247-
const SECTION = { label: 'Main', collapsible: false, collapsed: false, columns: 1, fields: ['name'] };
247+
// A section's `columns` has no default: an undeclared density is the renderer's,
248+
// so the parse adds no `columns` key (it added `columns: 1` before the default was dropped).
249+
const SECTION = { label: 'Main', collapsible: false, collapsed: false, fields: ['name'] };
248250
/** [#7741] The binding pair the write path inherits onto a shadowing overlay. */
249251
const BOUND_LIST = { object: 'crm_lead', viewKind: 'list' } as const;
250252
const BOUND_FORM = { object: 'crm_lead', viewKind: 'form' } as const;

‎packages/spec/src/ui/view.test.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,17 @@ describe('FormSectionSchema', () => {
625625
const result = FormSectionSchema.parse(section);
626626
expect(result.collapsible).toBe(false);
627627
expect(result.collapsed).toBe(false);
628-
expect(result.columns).toBe(1);
628+
// An undeclared density is the renderer's (by available width): the parse
629+
// adds no `columns`, so "said nothing" stays distinguishable from `1`.
630+
expect('columns' in result).toBe(false);
631+
});
632+
633+
it('keeps a declared `columns` as the author\'s ceiling — string spellings parse to the number, out-of-range is refused', () => {
634+
expect(FormSectionSchema.parse({ fields: ['name'], columns: 1 }).columns).toBe(1);
635+
expect(FormSectionSchema.parse({ fields: ['name'], columns: '2' }).columns).toBe(2);
636+
expect(FormSectionSchema.parse({ fields: ['name'], columns: 4 }).columns).toBe(4);
637+
expect(FormSectionSchema.safeParse({ fields: ['name'], columns: 5 }).success).toBe(false);
638+
expect(FormSectionSchema.safeParse({ fields: ['name'], columns: '0' }).success).toBe(false);
629639
});
630640

631641
it('should accept form section with all properties', () => {

0 commit comments

Comments
 (0)