Skip to content

Commit 3c7785d

Browse files
fix(rest,metadata-protocol): a public form's intake withdrawal at any metadata layer holds; layering can only narrow intake (#21864)
Fixes #21835 Clause-②: yes (widening) Fixes a regression introduced after 17.6.0 (with #21420); it should land before 17.7.0 is cut. ## What Per the rulings recorded on #21835: a public form's withdrawal is a kill switch, layering can only narrow anonymous intake, a withdrawal closes the **same form** only, and only an **explicit** withdrawal counts. - **Anonymous doors (`GET /forms/:slug`, `POST /forms/:slug/submit`).** Both use one resolver and judge by the name of the view item they serve. When an organization is resolved, the env-wide view list beneath it is read as well. A form is served only when the env-wide item of the same name does not explicitly withdraw a form in the same slot (nested form, the same `formViews` key, or the flattened config) or with the same slug. Other views that share the public slug never close each other. - **What counts as a withdrawal.** A sharing that keeps its `publicLink` and sets `enabled: false` or `allowAnonymous: false`. Only an explicit false counts. Not a withdrawal: an absent switch, a sharing with no link (raw, or schema-parsed), a cleared link, a removed sharing block, or no body of the view at that layer. The public data collection docs page has a "Withdraw a public form" section with these rules. - **Write door (save and publish).** An org-scoped `view` save or draft promotion in the organization the doors read is refused with `403 NOT_OVERRIDABLE` when it would leave open a form the env-wide definition explicitly withdraws. It judges by the stored row: the body is compared with the env-wide body of the row it is keyed by (the active env-wide row, else the package artifact), matched by slot or by slug. So renamed `formViews` keys, `form.name`, slot moves and listViews collision renames are the same form. It is also judged against the env-wide view list the way the doors read it, with container bodies expanded. Re-saving an overlay that was open before the withdrawal is refused. The message names both remedies. - **Package-shipped forms.** A package artifact is part of the env-wide definition, not a separate layer. A package artifact parsed by the stack schema (strict `defineStack`, the default) carries the schema's default `enabled: false`, so a shipped form that keeps its link without switching `enabled` on is an explicit withdrawal and fails closed. An artifact loaded without that parse (`defineStack(..., { strict: false })` or a hand-built manifest) is judged as written: a switch it omits is absent, which is not a withdrawal. The env-wide definition is the administrator's switch, so an env-wide save may open a form the package ships closed. - **Known limit: packages and names.** A withdrawal of a view name closes that name in every package: when two packages ship a view of the same name, one package's withdrawal also closes the other package's form of that name. It may over-close, never under-close. Per-package precision is tracked in #21934. A publish judges the draft it promotes under the same package key (the stated one, else the resolved draft row's own), so with two packages holding a draft of the same view in one organization, each draft is judged on its own publish. - **Intentional reversal.** The earlier behaviour in which an organization overlay re-published a form the package had withdrawn is reversed. A form with no env-wide word on it stays organization-publishable (#21420), and the #21473 anchors and #21566 field allowlist are unchanged. - **Public surface:** `@objectstack/metadata-core` adds one export, `anonymousFormIntakeWithdrawnIn` (`minor`). `@objectstack/rest` and `@objectstack/metadata-protocol` are `patch`. - **Known limit (ruled to stay as is).** The doors match by served item name, and the write door runs only on an org-scoped save or publish. An organization overlay stored before the env-wide withdrawal, or restored by rollback or commit revert, can still be served if it keeps the form open under a different key or slot than the env-wide definition. Withdrawing the form in that overlay closes it. Stated in the changeset and the docs. ## Tests The first bullet is round 6, the second round 5, the third round 4; the bullets after them were measured at `e8778acb96` (round 2): - Round 6 at `7882eef683` (merged origin/main `9dce635337`, merge commit `46d08189a7`): metadata-core 18 files, 411 passed; metadata-protocol 216 files (3 skipped), 27940 passed, 19 skipped; rest 260 files, 4912 passed, 326 skipped; objectql 375 files, 7469 passed (suites at `4d5f6c4e61`; the later commits touch docs, the changeset and three ported dogfood files only). Typecheck green for metadata-core, metadata-protocol, rest and objectql, test layers included. 97 of 97 derived gates green at `7882eef683`, reconciled with `dispatch-gates --ran`; `dispatch-gates --self-test` 1976 cases pass. The cross-package skip of round 5 is removed per the ruling, so a withdrawal of a view name closes it in every package again. Pins: another package's withdrawal of the same name closes this package's form too (metadata-core and the doors); with two packages shipping the same view name, a row-anchored rename by a package-bound org save is refused (metadata-protocol), with a withdrawn-save control. Ablation: the two edited sources set back to their round-5 blobs and rebuilt, markers proved in `dist/`: 1 red in each of metadata-protocol, metadata-core and rest; restored to HEAD (`git diff HEAD` empty), rebuilt, markers proved absent. - Round 5 at `d8657b5c19`: metadata-core 18 files, 411 passed; metadata-protocol 216 files (3 skipped), 27938 passed, 19 skipped; rest 260 files, 4911 passed, 326 skipped; objectql 374 files, 7464 passed. Typecheck green for metadata-core, metadata-protocol, rest and objectql, test layers included. 97 of 97 derived gates green, reconciled with `dispatch-gates --ran`. New pins: two packages' drafts of one view in one organization are each judged on their own publish; one package's withdrawal of a name closes its own form (metadata-core and both doors; the cross-package half was inverted in round 6). Ablation: removing the package key from the publish gate's draft read turned the two-package pin red, and removing the package comparison turned the cross-package pin red; both restored to HEAD (`git diff HEAD` empty). - Round 4 at `79b847042d` (targeted): metadata-core `anonymous-form-intake.test.ts` 39/39, metadata-protocol `protocol.org-scoped-write-refused.test.ts` 41/41, rest `public-form-withdrawal` + `public-form-intake-availability` 43/43. Typecheck green for metadata-core and metadata-protocol. Docs and changeset gates green. New pins: a package parsed `false` is a withdrawal; an env-wide save opens a package-closed form. - `@objectstack/metadata-core`: 18 files, 394 passed. - `@objectstack/rest`: 260 files, 4906 passed, 326 skipped. - `@objectstack/metadata-protocol`: 214 files (3 skipped), 27752 passed, 19 skipped. - Typecheck green for all three and dogfood. - Dogfood (real showcase boot): the layered-withdrawal suite 5/5 (including the re-save refusal) and the five sibling public-form suites 20/20. - Coverage: two views sharing a slug do not close each other (one read, with and without an organization, and across layers); a cleared link is not a withdrawal; a parsed link-less sharing is not a withdrawal; re-saving an already-open overlay is refused; a container-shaped save is judged after expansion. - Ablation (source set back to the base blobs, packages rebuilt): 3 / 4 / 4 tests red across metadata-core / rest / metadata-protocol; restored to HEAD. - Gates: 92 of 95 derived run green; `check:skill-examples`, `check:dual-build-cjs-loads` and `check:type-check-debt` are NOT MEASURED locally (workspace-wide prerequisites) and left to CI. ## Acceptance notes - The known limit above (an overlay stored before the withdrawal, or restored by rollback or revert, with its form under a different key or slot) is accepted per the ruling on #21835. No provenance or new protocol query was added. - Two installed apps publishing the same slug is a separate concern (slug collision), out of scope here. - A package artifact loaded without the stack schema's parse (`strict: false`, a hand-built manifest) is judged as written; giving every load path the schema's sharing defaults is left as a possible follow-up (see the round 5 report on #21835). - An objectql test double now answers the publish gate's draft-row read (`protocol-publish-package-drafts.test.ts`); that is test-only. - Maintainer ruling, 2026-10-06: 「撤掉跨包那一改,合并」. The cross-package skip is removed; per-package precision is tracked in #21934. - This branch carries three dogfood files ported unchanged from #21935 (`packages/qa/dogfood/test/per-file-cwd.setup.ts`, `packages/qa/dogfood/test/per-file-cwd.global-setup.ts`, `packages/qa/dogfood/vitest.config.ts`) so the dispatch-gates self-test is green here; they merge away once #21935 lands. --- _Generated by [Claude Code](https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f57627b commit 3c7785d

11 files changed

Lines changed: 1069 additions & 26 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/rest': patch
3+
'@objectstack/metadata-protocol': patch
4+
'@objectstack/metadata-core': minor
5+
---
6+
7+
A public form's explicit intake withdrawal at any metadata layer now holds: layering can only narrow anonymous intake, never re-open it
8+
9+
Clause-②: yes (widening)
10+
11+
- **What counts as a withdrawal.** A withdrawal keeps the form's `publicLink` and sets `sharing.enabled: false` or `sharing.allowAnonymous: false`. Only an explicit `false` counts: a switch that is absent is not a withdrawal. Removing the `sharing` block, clearing the `publicLink`, or deleting the view at one layer is not a withdrawal either. A sharing that names no public link withdraws nothing.
12+
- **Organization-scoped saves and publishes.** A `view` save or draft promotion in the organization the anonymous form doors read is refused with `403 NOT_OVERRIDABLE` if it would leave open a form that the environment-wide definition withdraws. This check judges by the stored row: the organization's body is compared with the env-wide body of the row it is keyed by (the active env-wide row, else the package's artifact), and also with the env-wide view list the way the doors read it (a container-shaped body is expanded the way the list read expands it). Inside the row, a withdrawn form matches by its place (`form`, the same `formViews` entry, or `config`) or by its public slug, and either match is enough. So a renamed `formViews` key, a `form.name`, a move to another place, a listViews collision rename in the expansion, and a new or re-cased slug are all judged as the same form. A form that differs from every withdrawn form in both place and slug, such as a sibling in the same container, stays independent. The check also covers an organization copy that was already open before the withdrawal, the next time it is saved. The message names the remedies: save the overlay withdrawn, or publish the form from its environment-wide definition. An organization-scoped save that keeps the form withdrawn is still accepted.
13+
- **Anonymous form doors.** `GET /forms/:slug` and `POST /forms/:slug/submit` judge by the name of the view item they serve. Beneath the organization's read they read the env-wide view list, and they serve a form only when the env-wide item of the same name does not explicitly withdraw a form in the same place or with the same slug. A withdrawn form answers `404 FORM_NOT_FOUND` on both doors and creates no record. A form that is open at every layer is served as before. A form that only an organization carries is still served there. A different view that uses the same slug is a different form, and the two never close each other.
14+
- **Package-shipped forms.** A package's form is part of the env-wide definition, not a separate layer beneath it. A package artifact that was parsed by the stack schema (strict `defineStack`, the default) carries the schema's default `enabled: false`, so a shipped form that keeps its link without switching `enabled` on is an explicit withdrawal (fail closed). An artifact that reached the runtime without that parse (`defineStack(..., { strict: false })` or a hand-built manifest) is judged as written: there a switch it omits is absent, which is not a withdrawal. The env-wide definition is the administrator's switch: an env-wide save may open a form the package ships closed.
15+
- **Known limit: packages and names.** A withdrawal of a view name closes that name in every package. When two packages ship a view of the same name, one package's withdrawal also closes the other package's form of that name: it may over-close, never under-close. Per-package precision is tracked in #21934. A publish judges the draft it promotes under the same package key: with two packages holding a draft of the same view in one organization, each draft is judged on its own publish.
16+
- **Known limit.** The doors match by served item name, and the save check runs only on an organization-scoped save or publish. An organization overlay that was stored before the env-wide withdrawal, or that a rollback or commit-revert restores, can still be served if it keeps the form open under a different key or place than the env-wide definition. Withdraw the form in that overlay to close it. Rollback and commit-revert restores are not gated by the save check.
17+
- **Behaviour change.** Between 17.6.0 and this fix, an organization overlay that published a form the environment-wide (package) definition withdrew was honoured: the doors served the organization's copy. That behaviour never shipped in a release, and it is reversed on purpose. The environment-wide withdrawal now wins.
18+
- **`@objectstack/metadata-core`** exports the shared judgement `anonymousFormIntakeWithdrawnIn` (a new, additive public export). Both the doors and the save path read it.

‎content/docs/ui/public-data-collection.mdx‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,26 @@ System-managed anchors (`owner_id`, `organization_id`, audit columns, `id`) are
5252

5353
Set `sharingModel: 'private'` on the object so submissions are staff-scoped after creation. The public path only ever **inserts**; it never lists.
5454

55+
### 4. Withdraw a public form
56+
57+
To stop taking submissions, keep the form's `publicLink` and set a switch to `false`: `enabled: false` or `allowAnonymous: false`. Both anonymous endpoints then answer `404 FORM_NOT_FOUND`, and nothing is created.
58+
59+
A withdrawal is a kill switch across metadata layers. If the environment-wide definition withdraws the form, an organization's copy of the same view cannot open it again: the endpoints keep answering not found, and an organization-scoped save or publish that would leave the form open is refused with `403 NOT_OVERRIDABLE`. To publish the form again, save it environment-wide with both switches on. An organization's copy can always withdraw the form for itself.
60+
61+
**What counts as a withdrawal.** Only an explicit `false` withdraws, on a sharing that keeps its `publicLink`. A switch that is simply absent is not a withdrawal. Removing the `sharing` block, clearing the `publicLink`, or deleting the view at one layer does not withdraw the form at the other layers. A form that only an organization publishes stays open there.
62+
63+
**Which form a withdrawal closes.** Two checks apply the rule, and they match forms differently:
64+
65+
- **Saving and publishing in an organization** judges the organization's copy against the stored environment-wide definition it overrides (the row its copy is keyed by). Inside that definition, a withdrawn form is the same form as the organization's when they share a place (`form`, the same `formViews` entry, or the view's own `config`) or a public link. A match on either is enough, so a renamed `formViews` key, a `form.name`, a move to another place, a renamed expanded item, and a new or re-cased slug all still count as the same form. A form that differs from every withdrawn form in both place and link is a different form, such as a sibling in the same view.
66+
- **The anonymous endpoints** judge each form by the name of the view item they serve. Beneath the organization's read they read the environment-wide view list, and a form is closed when the environment-wide item of the same name explicitly withdraws a form in the same place or under the same link.
67+
- **A different view is a different form.** A different view that uses the same public link (for example, another app's "contact us" form) neither closes this one nor is closed by it.
68+
69+
**Forms a package ships.** A package's form is part of the environment-wide definition, not a separate layer beneath it. A definition parsed by the stack schema (strict `defineStack`, the default) gets the schema's default `enabled: false`, so a shipped form that keeps its link without setting `enabled: true` counts as withdrawn and an organization's copy cannot open it. A definition loaded without that parse (`defineStack(..., { strict: false })` or a hand-built manifest) is judged as written: a switch it leaves out is absent, which is not a withdrawal, so set `enabled: false` explicitly to ship a form closed. The environment-wide definition is the administrator's switch: an environment-wide save may open a form that the package ships closed.
70+
71+
**Known limit: packages and names.** A withdrawal of a view name closes that name in every package. When two packages each ship a view of the same name, one package's withdrawal also closes the other package's form of that name. This may close more than was meant, but it never leaves a withdrawn form open. Per-package precision is tracked in #21934.
72+
73+
**Known limit.** The save check runs only when an organization's copy is saved or published. A copy that was already stored before the environment-wide withdrawal, or that a rollback or revert restores, is judged only by the endpoints, which match by served item name. If that copy keeps the form open under a different key or place than the environment-wide definition, the endpoints can still serve it. To close it, withdraw the form in that organization's copy too; the next organization-scoped save of a copy that keeps it open is refused.
74+
5575
## Why
5676

5777
Authorization is **derived from the declaration**, not configured separately — so the grant can't drift wider than the form. There is no standing "anonymous can write to this object" rule to misconfigure: the only thing the public can do is create one record through one whitelisted form. This is the difference from Airtable, where interfaces can't be shared publicly at all (only forms can) — here the same FormView metadata renders both internally (authed) and publicly (anonymous) through one renderer.

‎packages/metadata-core/src/anonymous-form-intake.test.ts‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
anonymousFormIntakeUnavailability,
1111
anonymousFormIntakeUnavailableMessage,
1212
anonymousFormIntakeUnavailableRemedy,
13+
anonymousFormIntakeWithdrawnIn,
1314
anonymousFormObjectName,
1415
anonymousFormSharingPath,
1516
publicFormSlug,
@@ -184,3 +185,133 @@ describe('where the reason is located, and the reason itself', () => {
184185
expect(message.endsWith(` ${anonymousFormIntakeUnavailableRemedy(u)}`)).toBe(true);
185186
});
186187
});
188+
189+
describe('anonymousFormIntakeWithdrawnIn — an explicit withdrawal of the same row\'s form at any layer closes it', () => {
190+
const view = (sharing: unknown, name = 'contact') => ({
191+
name, object: 'inquiry', viewKind: 'form', config: { sharing },
192+
});
193+
const openView = view(OPEN);
194+
const [candidate] = anonymousFormIntakeCandidates(openView);
195+
196+
it('the same row, the link kept with a switch explicitly false: withdrawn', () => {
197+
expect(anonymousFormIntakeWithdrawnIn([view({ ...OPEN, allowAnonymous: false })], openView, candidate)).toBe(true);
198+
expect(anonymousFormIntakeWithdrawnIn([view({ ...OPEN, enabled: false })], openView, candidate)).toBe(true);
199+
});
200+
201+
it('only an explicit false withdraws: an absent switch is not a withdrawal', () => {
202+
// publicLink + enabled:true, allowAnonymous absent: not a withdrawal.
203+
expect(anonymousFormIntakeWithdrawnIn(
204+
[view({ enabled: true, publicLink: '/forms/contact-us' })], openView, candidate,
205+
)).toBe(false);
206+
expect(anonymousFormIntakeWithdrawnIn(
207+
[view({ allowAnonymous: true, publicLink: '/forms/contact-us' })], openView, candidate,
208+
)).toBe(false);
209+
});
210+
211+
it('two different rows sharing a slug do not close each other', () => {
212+
const other = view({ ...OPEN, enabled: false }, 'legacy_contact');
213+
expect(anonymousFormIntakeWithdrawnIn([openView, other], openView, candidate)).toBe(false);
214+
const [otherOpen] = anonymousFormIntakeCandidates(view(OPEN, 'legacy_contact'));
215+
expect(anonymousFormIntakeWithdrawnIn(
216+
[view({ ...OPEN, enabled: false }), view(OPEN, 'legacy_contact')], view(OPEN, 'legacy_contact'), otherOpen,
217+
)).toBe(false);
218+
});
219+
220+
// Known limit (fails closed): the package a body is bound to is not
221+
// compared, so a withdrawal of a name closes that name in every package.
222+
describe('the package is not compared: a withdrawal of a name closes it in every package', () => {
223+
const bound = (body: Record<string, unknown>, pkg: string) => ({ ...body, _packageId: pkg });
224+
const withdrawn = view({ ...OPEN, enabled: false });
225+
226+
it('another package\'s withdrawal of the same name closes this package\'s form too', () => {
227+
const openA = bound(openView, 'pkg_a');
228+
const [c] = anonymousFormIntakeCandidates(openA);
229+
expect(anonymousFormIntakeWithdrawnIn([bound(withdrawn, 'pkg_b')], openA, c)).toBe(true);
230+
// Another package's OPEN body of the name withdraws nothing.
231+
expect(anonymousFormIntakeWithdrawnIn([bound(openView, 'pkg_b')], openA, c)).toBe(false);
232+
});
233+
234+
it('the same package\'s withdrawal of the same name closes it', () => {
235+
const openA = bound(openView, 'pkg_a');
236+
const [c] = anonymousFormIntakeCandidates(openA);
237+
expect(anonymousFormIntakeWithdrawnIn([bound(withdrawn, 'pkg_a')], openA, c)).toBe(true);
238+
// Beside another package's open body of that name: still closed.
239+
expect(anonymousFormIntakeWithdrawnIn([bound(openView, 'pkg_b'), bound(withdrawn, 'pkg_a')], openA, c))
240+
.toBe(true);
241+
});
242+
243+
it('a body bound to no package stands in for every package\'s row of the name, on either side', () => {
244+
const openA = bound(openView, 'pkg_a');
245+
const [c] = anonymousFormIntakeCandidates(openA);
246+
expect(anonymousFormIntakeWithdrawnIn([withdrawn], openA, c)).toBe(true);
247+
expect(anonymousFormIntakeWithdrawnIn([bound(withdrawn, 'pkg_b')], openView, candidate)).toBe(true);
248+
expect(anonymousFormIntakeWithdrawnIn([withdrawn], openView, candidate)).toBe(true);
249+
});
250+
});
251+
252+
it('not a withdrawal: no body of the row, no sharing, the link cleared', () => {
253+
expect(anonymousFormIntakeWithdrawnIn([openView], openView, candidate)).toBe(false);
254+
expect(anonymousFormIntakeWithdrawnIn([], openView, candidate)).toBe(false);
255+
expect(anonymousFormIntakeWithdrawnIn([view(undefined)], openView, candidate)).toBe(false);
256+
expect(anonymousFormIntakeWithdrawnIn([view({ enabled: false, allowAnonymous: false })], openView, candidate)).toBe(false);
257+
expect(anonymousFormIntakeWithdrawnIn([view({ ...OPEN, enabled: false, publicLink: '' })], openView, candidate)).toBe(false);
258+
});
259+
260+
it('a schema-parsed sharing with no public link is not a withdrawal, as its raw body is not', () => {
261+
for (const raw of [{ password: 'secret' }, { allowedDomains: ['example.com'] }, { enabled: true }]) {
262+
const parsed = SharingConfigSchema.parse(raw);
263+
expect(parsed.enabled === true && parsed.allowAnonymous === true).toBe(false);
264+
expect(anonymousFormIntakeWithdrawnIn([view(parsed)], openView, candidate)).toBe(false);
265+
expect(anonymousFormIntakeWithdrawnIn([view(raw)], openView, candidate)).toBe(false);
266+
}
267+
});
268+
269+
it('a schema-parsed `false` that keeps the link IS a withdrawal (a package artifact fails closed)', () => {
270+
// A package artifact is served as parsed, and the schema defaults
271+
// `enabled` to false: a shipped sharing that keeps its link and never
272+
// switches `enabled` on carries an explicit `false` once parsed. That is
273+
// a withdrawal. Its raw body, with the switch absent, is not one.
274+
const raw = { allowAnonymous: true, publicLink: '/forms/contact-us' };
275+
const parsed = SharingConfigSchema.parse(raw);
276+
expect(parsed.enabled).toBe(false);
277+
expect(anonymousFormIntakeWithdrawnIn([view(parsed)], openView, candidate)).toBe(true);
278+
expect(anonymousFormIntakeWithdrawnIn([view(raw)], openView, candidate)).toBe(false);
279+
});
280+
281+
it('the same slot with a new slug (case-only included) is the same form: closed', () => {
282+
const withdrawn = [view({ ...OPEN, enabled: false })];
283+
for (const link of ['/forms/contact-us-2', '/forms/Contact-Us']) {
284+
const moved = view({ ...OPEN, publicLink: link });
285+
const [c] = anonymousFormIntakeCandidates(moved);
286+
expect(anonymousFormIntakeWithdrawnIn(withdrawn, moved, c)).toBe(true);
287+
}
288+
});
289+
290+
describe('a container row: identity survives a key rename, form.name, a slot move and an expansion rename', () => {
291+
const LINK_A = { ...OPEN, publicLink: '/forms/a' };
292+
const LINK_B = { ...OPEN, publicLink: '/forms/b' };
293+
const row = (body: Record<string, unknown>) => ({ name: 'inquiry', object: 'inquiry', ...body });
294+
// Env-wide: formViews.a withdrawn, formViews.b open.
295+
const envRow = row({ formViews: { a: { sharing: { ...LINK_A, enabled: false } }, b: { sharing: LINK_B } } });
296+
const closedIn = (overlay: Record<string, unknown>) =>
297+
anonymousFormIntakeCandidates(overlay)
298+
.filter((c) => anonymousFormIntakeWithdrawnIn([envRow], overlay, c))
299+
.map((c) => c.slug);
300+
301+
it('a key rename keeps the slug: closed', () => {
302+
expect(closedIn(row({ formViews: { a2: { sharing: LINK_A }, b: { sharing: LINK_B } } }))).toEqual(['a']);
303+
});
304+
it('a slot move to the nested form, with a form.name: closed', () => {
305+
expect(closedIn(row({ form: { name: 'renamed', sharing: LINK_A }, formViews: { b: { sharing: LINK_B } } })))
306+
.toEqual(['a']);
307+
});
308+
it('a listViews entry that collides with the key (an expansion rename): closed', () => {
309+
expect(closedIn(row({ listViews: { a: { type: 'grid' } }, formViews: { a: { sharing: LINK_A } } })))
310+
.toEqual(['a']);
311+
});
312+
it('the sibling form (another slot and another slug) stays independent', () => {
313+
expect(closedIn(row({ formViews: { a: { sharing: { ...LINK_A, enabled: false } }, b: { sharing: LINK_B } } })))
314+
.toEqual([]);
315+
});
316+
});
317+
});

0 commit comments

Comments
 (0)