refactor: separate schema fields logic

This commit is contained in:
2026-09-26 09:08:28 +05:00
parent ca016ada43
commit cdb23fe1c1
8 changed files with 148 additions and 44 deletions
+2 -32
View File
@@ -4,6 +4,7 @@
FieldSpecKind,
ToolSchema,
} from "$lib/registry-schema";
import { resolveLayoutGroups } from "$lib/registry-schema";
import { fieldLabel, groupLabel } from "$lib/i18n/schema-tool-strings";
import { t } from "$lib/i18n/t";
import { RotateCcw } from "@lucide/svelte";
@@ -57,38 +58,7 @@
}
let { schema, values, toolId, onchange, onreset }: Props = $props();
interface LayoutGroup {
key: string;
title?: string;
cols: number;
fields: string[];
}
const layoutGroups = $derived.by((): LayoutGroup[] => {
const all = Object.keys(schema.fields);
const groups = schema.layout?.groups ?? [];
const used: Record<string, true> = {};
const named = groups
.map((g, gi) => {
const fields = g.fields.filter((f) => {
if (used[f] || !(f in schema.fields)) return false;
used[f] = true;
return true;
});
return {
key: `${g.title ?? "group"}-${gi}`,
title: g.title,
cols: Math.max(1, Math.trunc(g.cols ?? 1)),
fields,
} satisfies LayoutGroup;
})
.filter((g) => g.fields.length > 0);
const rest = all.filter((f) => !used[f]);
if (rest.length > 0) {
named.push({ key: "__default", title: undefined, cols: 1, fields: rest });
}
return named;
});
const layoutGroups = $derived(resolveLayoutGroups(schema));
</script>
{#each layoutGroups as group (group.key)}
+77
View File
@@ -3,6 +3,7 @@ import {
applySourceDefaults,
defaultSchemaParams,
field,
resolveLayoutGroups,
sanitizeSchemaParams,
toolSchema,
type Dimension,
@@ -130,6 +131,82 @@ describe("registry-schema: раскладка (schema.layout)", () => {
});
expect(plain.layout).toBeUndefined();
});
it("разрешает named-группы и хвост без layout", () => {
expect(resolveLayoutGroups(frameSchema)).toEqual([
{
key: "Frame-0",
title: "Frame",
cols: 1,
fields: ["thickness", "color"],
},
{ key: "__default", cols: 1, fields: ["enabled", "count"] },
]);
});
it("убирает неизвестные и повторные поля, нормализует cols", () => {
const schema = toolSchema<FrameParams>(
{
thickness: field.slider({ min: 1, max: 500, default: 5 }),
color: field.color({ default: "#000000" }),
enabled: field.checkbox({ default: true }),
count: field.select({
default: "two",
options: [
{ value: "one", label: "One" },
{ value: "two", label: "Two" },
],
}),
},
{
layout: {
groups: [
{
title: "Duplicate",
cols: 0,
fields: ["thickness", "thickness", "missing"],
},
{ title: "Empty", fields: ["missing"] },
{ cols: 3, fields: ["color"] },
],
},
},
);
expect(resolveLayoutGroups(schema)).toEqual([
{
key: "Duplicate-0",
title: "Duplicate",
cols: 1,
fields: ["thickness"],
},
{ key: "group-2", title: undefined, cols: 3, fields: ["color"] },
{ key: "__default", cols: 1, fields: ["enabled", "count"] },
]);
});
it("складывает все поля без layout в default-группу", () => {
const plain = toolSchema<FrameParams>({
thickness: field.slider({ min: 1, max: 500, default: 5 }),
color: field.color({ default: "#000000" }),
enabled: field.checkbox({ default: true }),
count: field.select({
default: "two",
options: [
{ value: "one", label: "One" },
{ value: "two", label: "Two" },
],
}),
});
expect(resolveLayoutGroups(plain)).toEqual([
{
key: "__default",
cols: 1,
fields: ["thickness", "color", "enabled", "count"],
},
]);
});
});
describe("registry-schema: санитайз", () => {
+35
View File
@@ -341,6 +341,41 @@ export function toolSchema<P>(
return { fields, ...(meta ?? {}) };
}
export interface ResolvedLayoutGroup {
key: string;
title?: string;
cols: number;
fields: string[];
}
export function resolveLayoutGroups<P>(
schema: ToolSchema<P>,
): ResolvedLayoutGroup[] {
const all = Object.keys(schema.fields);
const groups = schema.layout?.groups ?? [];
const used: Record<string, true> = {};
const named: ResolvedLayoutGroup[] = groups
.map((group, index) => {
const fields = group.fields.filter((fieldId) => {
if (used[fieldId] || !(fieldId in schema.fields)) return false;
used[fieldId] = true;
return true;
});
return {
key: `${group.title ?? "group"}-${index}`,
title: group.title,
cols: Math.max(1, Math.trunc(group.cols ?? 1)),
fields,
} satisfies ResolvedLayoutGroup;
})
.filter((group) => group.fields.length > 0);
const rest = all.filter((fieldId) => !used[fieldId]);
if (rest.length > 0) {
named.push({ key: "__default", cols: 1, fields: rest });
}
return named;
}
function clamp(value: number, min?: number, max?: number): number {
if (min !== undefined && value < min) return min;
if (max !== undefined && value > max) return max;