Skip to content
364 changes: 364 additions & 0 deletions HANDOFF.md

Large diffs are not rendered by default.

194 changes: 188 additions & 6 deletions packages/cli/src/commands/schema/init/generate-code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ describe("generateComponentFile", () => {
expect(result).not.toContain("restrict_type");
});

it("should keep restrict_components and restrict_type for group/tag restrictions", () => {
it("should keep restrict_components and restrict_type for an unresolvable group restriction", () => {
const component = {
id: 1,
name: "page",
Expand All @@ -167,6 +167,152 @@ describe("generateComponentFile", () => {
expect(result).toContain("restrict_type: 'groups',");
});

// Regression: `restrict_type` was dropped for tag restrictions too, so the
// round-trip pushed back a field whose tag lists the editor never reads. The
// tag dimension has no `allow`/`deny` equivalent, so nothing re-derives it.
it("should keep the tag lists and their flags for a tag restriction", () => {
const component = {
id: 1,
name: "page",
created_at: "",
updated_at: "",
schema: {
body: {
type: "bloks",
pos: 0,
restrict_components: true,
restrict_type: "tags",
component_tag_whitelist: [1, 2],
component_tag_denylist: [3],
},
},
};

const result = generateComponentFile(component as any);

expect(result).toContain("restrict_components: true,");
expect(result).toContain("restrict_type: 'tags',");
expect(result).toContain("component_tag_whitelist: [");
expect(result).toContain("component_tag_denylist: [");
expect(result).not.toContain("allow: [");
});

// Switching dimensions in the editor leaves the old lists behind. Emitting them
// as `allow` would make the next push re-derive `restrict_type: ''` and restrict
// the field by block name instead.
it("should drop a stale name list rather than emit allow when the tag dimension is in force", () => {
const component = {
id: 1,
name: "page",
created_at: "",
updated_at: "",
schema: {
body: {
type: "bloks",
pos: 0,
restrict_components: true,
restrict_type: "tags",
component_tag_whitelist: [1],
component_whitelist: ["hero"],
},
},
};

const result = generateComponentFile(component as any);

expect(result).toContain("restrict_type: 'tags',");
expect(result).not.toContain("allow: [");
expect(result).not.toContain("component_whitelist");
});

// Regression, reproduced against a real space: requiring a non-empty tag list to
// recognise the dimension dropped `restrict_type` and `restrict_components` for a
// tag restriction whose lists happen to be empty, and nothing re-derives them —
// `schema init` emitted a bare `defineField('content', { type: 'bloks' })` and the
// next push unrestricted the field.
it("should keep the tag dimension when its lists are empty", () => {
const component = {
id: 1,
name: "page",
created_at: "",
updated_at: "",
schema: {
body: {
type: "bloks",
pos: 0,
restrict_components: true,
restrict_type: "tags",
component_tag_whitelist: [],
component_tag_denylist: [],
},
},
};

const result = generateComponentFile(component as any);

expect(result).toContain("restrict_type: 'tags',");
expect(result).toContain("restrict_components: true,");
expect(result).not.toContain("allow: [");
});

// The editor clears all six lists when you switch dimension, and the Management
// API only strips stale name lists on `bloks` fields, so a `richtext` can carry
// `restrict_type: 'tags'` next to a live name list. With no tag selected the tag
// dimension restricts nothing, so claiming the field for it would drop the only
// list actually in force.
it("should keep a name list on a richtext whose tag dimension selects nothing", () => {
const component = {
id: 1,
name: "page",
created_at: "",
updated_at: "",
schema: {
body: {
type: "richtext",
pos: 0,
restrict_components: true,
restrict_type: "tags",
component_tag_whitelist: [],
component_whitelist: ["hero"],
},
},
};

const result = generateComponentFile(component as any);

expect(result).toContain("allow: [");
expect(result).toContain("'hero'");
expect(result).not.toContain("component_whitelist");
});

it("should not treat tag lists as a restriction when restrict_type does not select them", () => {
const component = {
id: 1,
name: "page",
created_at: "",
updated_at: "",
schema: {
body: {
type: "bloks",
pos: 0,
restrict_components: true,
restrict_type: "",
component_tag_whitelist: [1],
},
},
};

const result = generateComponentFile(component as any);

// The tag list passes through verbatim but nothing puts it in force, so it is
// not treated as the tag dimension. `restrict_components: true` is still real
// state with nothing to re-derive it, so it round-trips with its selector.
expect(result).toContain("component_tag_whitelist: [");
expect(result).toContain("restrict_components: true,");
expect(result).toContain("restrict_type: '',");
expect(result).not.toContain("allow: [");
});

it("should resolve a group whitelist to allow: [folderVar] and import the folder when uuids are known", () => {
const component = {
id: 1,
Expand Down Expand Up @@ -386,10 +532,11 @@ describe("generateComponentFile", () => {
expect(result).not.toContain("component_denylist");
});

it("should drop orphaned restrict flags when a restricted field has no names and no groups", () => {
// `restrict_components: true` with an empty `component_whitelist` and no group
// whitelist is a wire byproduct that `allow` re-derives on push; without an
// allow to back it, it must not be emitted as orphaned DSL state.
it("should keep `restrict_components: true` when no list is in force", () => {
// Regression: this was treated as a wire byproduct that `allow` re-derives on
// push, but with an empty `component_whitelist` there is no `allow` to emit and
// so nothing re-derives it. Dropping it switched the restriction off on the
// round-trip, turning "restricted, nothing selected" into "unrestricted".
const component = {
id: 1,
name: "landing",
Expand All @@ -408,11 +555,46 @@ describe("generateComponentFile", () => {
const result = generateComponentFile(component as any);

expect(result).toContain("defineField('body', {");
expect(result).not.toContain("restrict_components");
expect(result).toContain("restrict_components: true,");
// The empty list carries no dimension, so it is not emitted either way.
expect(result).not.toContain("component_whitelist");
expect(result).not.toContain("allow");
});

it("should not emit restriction keys for a field type that does not own them", () => {
// The Management API stores a component schema as an opaque blob, so a space
// can hold a stray `restrict_components` on an `asset` field. `defineField`
// rejects an option the field type does not own, so emitting it verbatim
// generated code that did not compile.
const component = {
id: 1,
name: "landing",
created_at: "",
updated_at: "",
schema: {
disabled_pic: { type: "asset", pos: 0, restrict_components: false, restrict_type: "" },
active_pic: { type: "asset", pos: 1, restrict_components: true, restrict_type: "" },
tagged_pic: {
type: "asset",
pos: 2,
restrict_type: "tags",
component_tag_whitelist: [1],
},
tagged_text: { type: "text", pos: 3, component_tag_whitelist: [1] },
grouped_pic: { type: "asset", pos: 4, component_group_whitelist: ["uuid"] },
},
};

const result = generateComponentFile(component as any);

expect(result).not.toContain("restrict_components");
expect(result).not.toContain("restrict_type");
// The tag lists reach the output through the untouched-key passthrough rather
// than the restriction branches, so they need the same guard.
expect(result).not.toContain("component_tag_whitelist");
expect(result).not.toContain("component_group_whitelist");
});

it("should keep a disabled restriction disabled instead of mapping a stale name whitelist to allow", () => {
// A field whose restriction is off can still store a whitelist. Emitting it as
// `allow` would make push re-derive `restrict_components: true`, silently
Expand Down
Loading
Loading