Render 1C form items hierarchically
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-21 07:25:28 +03:00
parent 8b9a076d86
commit a5e0c8bf0f
@@ -3075,7 +3075,7 @@ function FormDesignerPanel({
const updateElement = (id: string, patch: Partial<IdeFormElementDraft>) => { const updateElement = (id: string, patch: Partial<IdeFormElementDraft>) => {
setElementDrafts((current) => ({ setElementDrafts((current) => ({
...current, ...current,
[formKey]: (current[formKey] ?? baseElements).map((element) => (element.id === id ? { ...element, ...patch } : element)) [formKey]: updateIdeFormElementTree(current[formKey] ?? baseElements, id, patch)
})); }));
}; };
@@ -3090,7 +3090,8 @@ function FormDesignerPanel({
caption: name, caption: name,
controlKind: newElementKind, controlKind: newElementKind,
binding: name, binding: name,
width: "stretch" width: "stretch",
children: []
}; };
setElementDrafts((current) => ({ ...current, [formKey]: [...(current[formKey] ?? baseElements), next] })); setElementDrafts((current) => ({ ...current, [formKey]: [...(current[formKey] ?? baseElements), next] }));
setNewElementName(""); setNewElementName("");
@@ -3130,10 +3131,10 @@ function FormDesignerPanel({
</div> </div>
<div className="mt-5 border-t border-border pt-3 text-xs font-semibold uppercase text-muted-foreground">{t.formElements}</div> <div className="mt-5 border-t border-border pt-3 text-xs font-semibold uppercase text-muted-foreground">{t.formElements}</div>
<div className="mt-3 space-y-1"> <div className="mt-3 space-y-1">
{elements.length === 0 ? ( {flattenIdeFormElements(elements).length === 0 ? (
<div className="text-sm text-muted-foreground">{t.none}</div> <div className="text-sm text-muted-foreground">{t.none}</div>
) : ( ) : (
elements.map((element) => ( flattenIdeFormElements(elements).map((element) => (
<div className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-background" key={element.id}> <div className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-background" key={element.id}>
<OneCTreeIcon kind={element.controlKind === "table" ? "tabular" : "attribute"} /> <OneCTreeIcon kind={element.controlKind === "table" ? "tabular" : "attribute"} />
<div className="min-w-0"> <div className="min-w-0">
@@ -3239,7 +3240,7 @@ function FormDesignerPanel({
</label> </label>
</div> </div>
<div className="grid grid-cols-2 gap-2 border-b border-border p-3"> <div className="grid grid-cols-2 gap-2 border-b border-border p-3">
<IdeFormMetric label="elements" value={elements.length} /> <IdeFormMetric label="elements" value={flattenIdeFormElements(elements).length} />
<IdeFormMetric label="commands" value={commands.length} /> <IdeFormMetric label="commands" value={commands.length} />
</div> </div>
<div className="border-b border-border p-3"> <div className="border-b border-border p-3">
@@ -3260,7 +3261,7 @@ function FormDesignerPanel({
</button> </button>
</div> </div>
<div className="divide-y divide-border"> <div className="divide-y divide-border">
{elements.map((element) => ( {flattenIdeFormElements(elements).map((element) => (
<div className="grid gap-2 p-3" key={`props-${element.id}`}> <div className="grid gap-2 p-3" key={`props-${element.id}`}>
<div className="truncate text-xs font-semibold">{element.name}</div> <div className="truncate text-xs font-semibold">{element.name}</div>
<input className="h-8 border border-border bg-background px-2 text-sm text-foreground" value={element.caption} onChange={(event) => updateElement(element.id, { caption: event.target.value })} /> <input className="h-8 border border-border bg-background px-2 text-sm text-foreground" value={element.caption} onChange={(event) => updateElement(element.id, { caption: event.target.value })} />
@@ -3296,10 +3297,41 @@ type IdeFormElementDraft = {
controlKind: "input" | "date" | "checkbox" | "table" | "group" | "text"; controlKind: "input" | "date" | "checkbox" | "table" | "group" | "text";
binding: string; binding: string;
width: "stretch" | "half" | "third"; width: "stretch" | "half" | "third";
qualifiedName?: string;
parentQualifiedName?: string | null;
children: IdeFormElementDraft[];
}; };
function IdeFormControl({ element, forceHalf }: Readonly<{ element: IdeFormElementDraft; forceHalf: boolean }>) { function IdeFormControl({ element, forceHalf }: Readonly<{ element: IdeFormElementDraft; forceHalf: boolean }>) {
const span = element.controlKind === "table" || element.controlKind === "group" ? "col-span-12" : forceHalf || element.width === "half" ? "col-span-6" : element.width === "third" ? "col-span-4" : "col-span-12"; const span = element.controlKind === "table" || element.controlKind === "group" || element.controlKind === "text" ? "col-span-12" : forceHalf || element.width === "half" ? "col-span-6" : element.width === "third" ? "col-span-4" : "col-span-12";
if (element.controlKind === "table") {
return (
<section className={`${span} border border-[#aeb8c6] bg-white`} data-ide-form-element={element.id} data-ide-form-control={element.controlKind}>
<div className="flex min-h-8 items-center justify-between border-b border-[#ccd4df] bg-[#eef2f7] px-2 text-xs font-semibold text-[#1f2937]">
<span>{element.caption}</span>
<span className="text-[11px] font-medium text-[#687385]">{element.binding}</span>
</div>
{ideFormControlInput(element)}
</section>
);
}
if (element.controlKind === "group") {
return (
<fieldset className={`${span} border border-[#b9c1cd] bg-[#f6f8fb] px-3 pb-3 pt-2`} data-ide-form-element={element.id} data-ide-form-control={element.controlKind}>
<legend className="px-1 text-xs font-semibold text-[#4b5563]">{element.caption}</legend>
<div className="grid grid-cols-12 gap-x-3 gap-y-2">
{element.children.length ? element.children.map((child) => <IdeFormControl element={child} forceHalf={forceHalf} key={child.id} />) : null}
</div>
</fieldset>
);
}
if (element.controlKind === "text") {
return (
<div className={`${span} border border-transparent px-1 py-1`} data-ide-form-element={element.id} data-ide-form-control={element.controlKind}>
{ideFormControlInput(element)}
</div>
);
}
return ( return (
<div className={`${span} grid min-h-8 grid-cols-[150px_minmax(0,1fr)] items-center gap-2 border border-transparent px-1 py-1 hover:border-[#b9c1cd] hover:bg-white`} data-ide-form-element={element.id} data-ide-form-control={element.controlKind}> <div className={`${span} grid min-h-8 grid-cols-[150px_minmax(0,1fr)] items-center gap-2 border border-transparent px-1 py-1 hover:border-[#b9c1cd] hover:bg-white`} data-ide-form-element={element.id} data-ide-form-control={element.controlKind}>
<label className="truncate text-xs font-semibold text-[#4b5563]">{element.caption}</label> <label className="truncate text-xs font-semibold text-[#4b5563]">{element.caption}</label>
@@ -3310,15 +3342,24 @@ function IdeFormControl({ element, forceHalf }: Readonly<{ element: IdeFormEleme
function ideFormControlInput(element: IdeFormElementDraft) { function ideFormControlInput(element: IdeFormElementDraft) {
if (element.controlKind === "table") { if (element.controlKind === "table") {
const columns = element.children.filter((child) => child.controlKind !== "text" && child.controlKind !== "group");
const visibleColumns = columns.length ? columns : [{ ...element, caption: element.caption, name: element.name, id: `${element.id}.column`, children: [] }];
return ( return (
<div className="min-h-28 border border-[#aeb8c6] bg-white text-xs"> <div className="min-h-32 overflow-hidden bg-white text-xs">
<div className="grid grid-cols-[2fr_1fr_1fr] bg-[#eef2f7] font-semibold"> <div className="grid" style={{ gridTemplateColumns: `repeat(${Math.min(visibleColumns.length, 8)}, minmax(120px, 1fr))` }}>
<span className="border-b border-r border-[#d7dde6] px-2 py-1">{element.binding}</span> {visibleColumns.slice(0, 8).map((column) => (
<span className="border-b border-r border-[#d7dde6] px-2 py-1">Количество</span> <span className="min-h-7 border-b border-r border-[#d7dde6] bg-[#f4f6f9] px-2 py-1 font-semibold text-[#374151] last:border-r-0" key={column.id}>
<span className="border-b border-[#d7dde6] px-2 py-1">Сумма</span> {column.caption}
</span>
))}
</div> </div>
<div className="grid grid-cols-[2fr_1fr_1fr]"><span className="min-h-7 border-r border-[#d7dde6]" /><span className="border-r border-[#d7dde6]" /><span /></div> {[0, 1, 2].map((row) => (
<div className="grid grid-cols-[2fr_1fr_1fr] border-t border-[#d7dde6]"><span className="min-h-7 border-r border-[#d7dde6]" /><span className="border-r border-[#d7dde6]" /><span /></div> <div className="grid" key={row} style={{ gridTemplateColumns: `repeat(${Math.min(visibleColumns.length, 8)}, minmax(120px, 1fr))` }}>
{visibleColumns.slice(0, 8).map((column) => (
<span className="min-h-7 border-b border-r border-[#edf1f6] px-2 py-1 last:border-r-0" key={`${row}-${column.id}`} />
))}
</div>
))}
</div> </div>
); );
} }
@@ -3329,7 +3370,7 @@ function ideFormControlInput(element: IdeFormElementDraft) {
return <div className="min-h-11 border border-dashed border-[#aeb8c6] bg-[#f6f8fb] px-2 py-2 text-xs font-medium text-muted-foreground">{element.binding}</div>; return <div className="min-h-11 border border-dashed border-[#aeb8c6] bg-[#f6f8fb] px-2 py-2 text-xs font-medium text-muted-foreground">{element.binding}</div>;
} }
if (element.controlKind === "text") { if (element.controlKind === "text") {
return <textarea className="min-h-16 resize-none border border-[#aeb8c6] bg-white px-2 py-1 text-xs" readOnly value={element.binding} />; return <div className="whitespace-pre-wrap border border-transparent bg-transparent px-1 py-1 text-xs leading-5 text-[#374151]">{element.caption || element.binding}</div>;
} }
return <input className="h-7 border border-[#aeb8c6] bg-white px-2 text-xs" readOnly value={element.binding} />; return <input className="h-7 border border-[#aeb8c6] bg-white px-2 text-xs" readOnly value={element.binding} />;
} }
@@ -3346,18 +3387,69 @@ function IdeFormMetric({ label, value }: Readonly<{ label: string; value: number
function buildIdeFormElements(form: ProjectWorkspaceData["forms"][number] | undefined): IdeFormElementDraft[] { function buildIdeFormElements(form: ProjectWorkspaceData["forms"][number] | undefined): IdeFormElementDraft[] {
const explicitElements = form?.elements ?? []; const explicitElements = form?.elements ?? [];
if (explicitElements.length) { if (explicitElements.length) {
return explicitElements.map((element, index) => ({ const formQualifiedName = form?.form.qualified_name ?? "";
const drafts = explicitElements
.filter((element) => formElementString(element.attributes, ["visible"]) !== "false")
.map((element, index) => {
const qualifiedName = element.qualified_name ?? element.name;
return {
id: element.lineage_id || `element.${index}`, id: element.lineage_id || `element.${index}`,
name: element.name, name: element.name,
caption: formElementString(element.attributes, ["caption", "title", "synonym"]) ?? element.name, caption: formElementCaption(element),
controlKind: controlKindForFormNode(element.name, formElementString(element.attributes, ["control_kind", "control", "type", "kind"]) ?? element.kind), controlKind: controlKindForFormNode(element.name, formElementString(element.attributes, ["control_kind", "control", "type", "kind"]) ?? element.kind),
binding: formElementString(element.attributes, ["binding", "dataPath", "data_path", "path"]) ?? element.qualified_name ?? element.name, binding: formElementString(element.attributes, ["binding", "dataPath", "data_path", "path"]) ?? qualifiedName ?? element.name,
width: formElementWidth(element.attributes, index) width: formElementWidth(element.attributes, index),
})); qualifiedName,
parentQualifiedName: parentQualifiedNameForElement(qualifiedName, formQualifiedName),
children: []
} satisfies IdeFormElementDraft;
});
return nestIdeFormElements(drafts);
} }
return []; return [];
} }
function formElementCaption(element: ProjectWorkspaceData["forms"][number]["elements"][number]) {
const title = formElementString(element.attributes, ["caption", "title", "synonym"]);
if (title) return title;
const dataPath = formElementString(element.attributes, ["dataPath", "data_path", "path"]);
if (dataPath?.includes(".")) return dataPath.split(".").at(-1) ?? element.name;
return element.name;
}
function parentQualifiedNameForElement(qualifiedName: string, formQualifiedName: string) {
const dot = qualifiedName.lastIndexOf(".");
if (dot < 0) return null;
const parent = qualifiedName.slice(0, dot);
return parent === formQualifiedName ? null : parent;
}
function nestIdeFormElements(elements: IdeFormElementDraft[]) {
const byQualifiedName = new Map(elements.map((element) => [element.qualifiedName, element]));
const roots: IdeFormElementDraft[] = [];
for (const element of elements) {
const parent = element.parentQualifiedName ? byQualifiedName.get(element.parentQualifiedName) : null;
if (parent) {
parent.children.push(element);
} else {
roots.push(element);
}
}
return roots;
}
function flattenIdeFormElements(elements: IdeFormElementDraft[]): IdeFormElementDraft[] {
return elements.flatMap((element) => [element, ...flattenIdeFormElements(element.children)]);
}
function updateIdeFormElementTree(elements: IdeFormElementDraft[], id: string, patch: Partial<IdeFormElementDraft>): IdeFormElementDraft[] {
return elements.map((element) => (
element.id === id
? { ...element, ...patch }
: { ...element, children: updateIdeFormElementTree(element.children, id, patch) }
));
}
function formElementString(attributes: Record<string, unknown>, keys: string[]): string | null { function formElementString(attributes: Record<string, unknown>, keys: string[]): string | null {
for (const key of keys) { for (const key of keys) {
const value = attributes[key]; const value = attributes[key];
@@ -3378,6 +3470,8 @@ function formElementWidth(attributes: Record<string, unknown>, index: number): I
function controlKindForFormNode(name: string, kind: string): IdeFormElementDraft["controlKind"] { function controlKindForFormNode(name: string, kind: string): IdeFormElementDraft["controlKind"] {
const normalized = `${name} ${kind}`.toLowerCase(); const normalized = `${name} ${kind}`.toLowerCase();
if (normalized.includes("decoration") || normalized.includes("label") || normalized.includes("надп")) return "text";
if (normalized.includes("group") || normalized.includes("группа") || normalized.includes("pages") || normalized.includes("page")) return "group";
if (normalized.includes("таб") || normalized.includes("table")) return "table"; if (normalized.includes("таб") || normalized.includes("table")) return "table";
if (normalized.includes("дата") || normalized.includes("date")) return "date"; if (normalized.includes("дата") || normalized.includes("date")) return "date";
if (normalized.includes("флаг") || normalized.includes("boolean") || normalized.includes("булево")) return "checkbox"; if (normalized.includes("флаг") || normalized.includes("boolean") || normalized.includes("булево")) return "checkbox";