Render 1C form items hierarchically
This commit is contained in:
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user