Render 1C form items hierarchically
This commit is contained in:
@@ -3075,7 +3075,7 @@ function FormDesignerPanel({
|
||||
const updateElement = (id: string, patch: Partial<IdeFormElementDraft>) => {
|
||||
setElementDrafts((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,
|
||||
controlKind: newElementKind,
|
||||
binding: name,
|
||||
width: "stretch"
|
||||
width: "stretch",
|
||||
children: []
|
||||
};
|
||||
setElementDrafts((current) => ({ ...current, [formKey]: [...(current[formKey] ?? baseElements), next] }));
|
||||
setNewElementName("");
|
||||
@@ -3130,10 +3131,10 @@ function FormDesignerPanel({
|
||||
</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">
|
||||
{elements.length === 0 ? (
|
||||
{flattenIdeFormElements(elements).length === 0 ? (
|
||||
<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}>
|
||||
<OneCTreeIcon kind={element.controlKind === "table" ? "tabular" : "attribute"} />
|
||||
<div className="min-w-0">
|
||||
@@ -3239,7 +3240,7 @@ function FormDesignerPanel({
|
||||
</label>
|
||||
</div>
|
||||
<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} />
|
||||
</div>
|
||||
<div className="border-b border-border p-3">
|
||||
@@ -3260,7 +3261,7 @@ function FormDesignerPanel({
|
||||
</button>
|
||||
</div>
|
||||
<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="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 })} />
|
||||
@@ -3296,10 +3297,41 @@ type IdeFormElementDraft = {
|
||||
controlKind: "input" | "date" | "checkbox" | "table" | "group" | "text";
|
||||
binding: string;
|
||||
width: "stretch" | "half" | "third";
|
||||
qualifiedName?: string;
|
||||
parentQualifiedName?: string | null;
|
||||
children: IdeFormElementDraft[];
|
||||
};
|
||||
|
||||
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 (
|
||||
<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>
|
||||
@@ -3310,15 +3342,24 @@ function IdeFormControl({ element, forceHalf }: Readonly<{ element: IdeFormEleme
|
||||
|
||||
function ideFormControlInput(element: IdeFormElementDraft) {
|
||||
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 (
|
||||
<div className="min-h-28 border border-[#aeb8c6] bg-white text-xs">
|
||||
<div className="grid grid-cols-[2fr_1fr_1fr] bg-[#eef2f7] font-semibold">
|
||||
<span className="border-b border-r border-[#d7dde6] px-2 py-1">{element.binding}</span>
|
||||
<span className="border-b border-r border-[#d7dde6] px-2 py-1">Количество</span>
|
||||
<span className="border-b border-[#d7dde6] px-2 py-1">Сумма</span>
|
||||
<div className="min-h-32 overflow-hidden bg-white text-xs">
|
||||
<div className="grid" 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-[#d7dde6] bg-[#f4f6f9] px-2 py-1 font-semibold text-[#374151] last:border-r-0" key={column.id}>
|
||||
{column.caption}
|
||||
</span>
|
||||
))}
|
||||
</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>
|
||||
<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>
|
||||
{[0, 1, 2].map((row) => (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
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} />;
|
||||
}
|
||||
@@ -3346,18 +3387,69 @@ function IdeFormMetric({ label, value }: Readonly<{ label: string; value: number
|
||||
function buildIdeFormElements(form: ProjectWorkspaceData["forms"][number] | undefined): IdeFormElementDraft[] {
|
||||
const explicitElements = form?.elements ?? [];
|
||||
if (explicitElements.length) {
|
||||
return explicitElements.map((element, index) => ({
|
||||
id: element.lineage_id || `element.${index}`,
|
||||
name: element.name,
|
||||
caption: formElementString(element.attributes, ["caption", "title", "synonym"]) ?? element.name,
|
||||
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,
|
||||
width: formElementWidth(element.attributes, 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}`,
|
||||
name: element.name,
|
||||
caption: formElementCaption(element),
|
||||
controlKind: controlKindForFormNode(element.name, formElementString(element.attributes, ["control_kind", "control", "type", "kind"]) ?? element.kind),
|
||||
binding: formElementString(element.attributes, ["binding", "dataPath", "data_path", "path"]) ?? qualifiedName ?? element.name,
|
||||
width: formElementWidth(element.attributes, index),
|
||||
qualifiedName,
|
||||
parentQualifiedName: parentQualifiedNameForElement(qualifiedName, formQualifiedName),
|
||||
children: []
|
||||
} satisfies IdeFormElementDraft;
|
||||
});
|
||||
return nestIdeFormElements(drafts);
|
||||
}
|
||||
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 {
|
||||
for (const key of keys) {
|
||||
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"] {
|
||||
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("date")) return "date";
|
||||
if (normalized.includes("флаг") || normalized.includes("boolean") || normalized.includes("булево")) return "checkbox";
|
||||
|
||||
Reference in New Issue
Block a user