Render forms from indexed form elements
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-21 05:30:53 +03:00
parent d26aaef44a
commit 5bd188fe6f
5 changed files with 86 additions and 83 deletions
@@ -3049,7 +3049,7 @@ function FormDesignerPanel({
const form = objectForms.find((item) => item.form.lineage_id === selectedFormId) ?? objectForms[0];
const commands = form?.commands.slice(0, 6) ?? [];
const formKey = form?.form.lineage_id ?? "draft";
const baseElements = useMemo(() => buildIdeFormElements(data, form), [data, form]);
const baseElements = useMemo(() => buildIdeFormElements(form), [form]);
const elements = elementDrafts[formKey] ?? baseElements;
const formTitle = titleByForm[formKey] ?? form?.form.name ?? "ФормаДокумента";
const formObjectCaption = language === "ru" ? `${formTitle} (форма 1С 8.5)` : `${formTitle} (1C 8.5 form)`;
@@ -3057,9 +3057,9 @@ function FormDesignerPanel({
useEffect(() => {
if (form && !elementDrafts[form.form.lineage_id]) {
setElementDrafts((current) => ({ ...current, [form.form.lineage_id]: buildIdeFormElements(data, form) }));
setElementDrafts((current) => ({ ...current, [form.form.lineage_id]: buildIdeFormElements(form) }));
}
}, [data, elementDrafts, form]);
}, [elementDrafts, form]);
const updateElement = (id: string, patch: Partial<IdeFormElementDraft>) => {
setElementDrafts((current) => ({
@@ -3175,9 +3175,23 @@ function FormDesignerPanel({
"grid grid-cols-12 gap-x-3 gap-y-2 p-5",
layout === "compact" ? "gap-y-1" : ""
].join(" ")}>
{elements.map((element) => (
<IdeFormControl element={element} forceHalf={layout === "columns"} key={element.id} />
))}
{elements.length ? (
elements.map((element) => (
<IdeFormControl element={element} forceHalf={layout === "columns"} key={element.id} />
))
) : (
<div className="col-span-12 border border-dashed border-[#aeb8c6] bg-white px-4 py-6 text-sm text-slate-600" data-ide-form-empty>
<div className="font-semibold text-slate-900">{language === "ru" ? "Структура элементов формы не загружена" : "Form element structure is not loaded"}</div>
<div className="mt-1 text-xs">
{form?.form.qualified_name ?? form?.form.name ?? data.projectId}
</div>
<div className="mt-3 text-xs">
{language === "ru"
? "В текущем индексе для этой формы нет узлов элементов. SFERA не подставляет шаблонные поля, чтобы не искажать объект 1С."
: "The current index has no element nodes for this form. SFERA does not insert template fields because that would distort the 1C object."}
</div>
</div>
)}
</div>
<div className="mt-auto border-t border-[#ccd4df] bg-white px-4 py-3">
@@ -3306,7 +3320,7 @@ function ideFormControlInput(element: IdeFormElementDraft) {
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 <input className="h-7 border border-[#aeb8c6] bg-white px-2 text-xs" readOnly value={element.controlKind === "date" ? "21.05.2026 0:00:00" : element.binding} />;
return <input className="h-7 border border-[#aeb8c6] bg-white px-2 text-xs" readOnly value={element.binding} />;
}
function IdeFormMetric({ label, value }: Readonly<{ label: string; value: number }>) {
@@ -3318,7 +3332,7 @@ function IdeFormMetric({ label, value }: Readonly<{ label: string; value: number
);
}
function buildIdeFormElements(data: ProjectWorkspaceData, form: ProjectWorkspaceData["forms"][number] | undefined): IdeFormElementDraft[] {
function buildIdeFormElements(form: ProjectWorkspaceData["forms"][number] | undefined): IdeFormElementDraft[] {
const explicitElements = form?.elements ?? [];
if (explicitElements.length) {
return explicitElements.map((element, index) => ({
@@ -3330,38 +3344,7 @@ function buildIdeFormElements(data: ProjectWorkspaceData, form: ProjectWorkspace
width: "stretch"
}));
}
const attributes = data.selectedObjectSchema?.attributes ?? [];
const tabularSections = data.selectedObjectSchema?.tabular_sections ?? [];
const fromSchema: IdeFormElementDraft[] = [
...attributes.map((attribute, index) => ({
id: attribute.lineage_id || `attribute.${index}`,
name: attribute.name,
caption: attribute.name,
controlKind: controlKindForFormNode(attribute.name, attribute.kind),
binding: attribute.name,
width: index < 2 ? "half" : "stretch"
} satisfies IdeFormElementDraft)),
...tabularSections.map((section, index) => ({
id: section.tabular_section.lineage_id || `table.${index}`,
name: section.tabular_section.name,
caption: section.tabular_section.name,
controlKind: "table" as const,
binding: section.tabular_section.name,
width: "stretch" as const
}))
];
if (fromSchema.length) {
return fromSchema;
}
const formName = form?.form.name ?? "ФормаДокумента";
if (formName.includes("ФормаДокумента")) {
return [
{ id: "default.number", name: "Номер", caption: "Номер", controlKind: "input", binding: "Объект.Номер", width: "half" },
{ id: "default.date", name: "Дата", caption: "Дата", controlKind: "date", binding: "Объект.Дата", width: "half" },
{ id: "default.table", name: "Товары", caption: "Товары", controlKind: "table", binding: "Объект.Товары", width: "stretch" }
];
}
return [{ id: "default.name", name: "Наименование", caption: "Наименование", controlKind: "input", binding: "Объект.Наименование", width: "stretch" }];
return [];
}
function controlKindForFormNode(name: string, kind: string): IdeFormElementDraft["controlKind"] {
@@ -5531,7 +5531,7 @@ function FormDesignerPanel({ projectId, object }: Readonly<{ projectId: string;
const [extraElementsByForm, setExtraElementsByForm] = useState<Record<string, FormDesignerElementDraft[]>>({});
const [newElementName, setNewElementName] = useState("");
const [newElementKind, setNewElementKind] = useState<FormDesignerElementDraft["kind"]>("input");
const baseElements = activeForm ? buildFormDesignerElements(object, activeForm) : [];
const baseElements = activeForm ? buildFormDesignerElements(activeForm) : [];
const elements = [...baseElements, ...(extraElementsByForm[activeFormKey] ?? [])].map((element) => ({
...element,
...(elementOverrides[element.id] ?? {})
@@ -5617,9 +5617,17 @@ function FormDesignerPanel({ projectId, object }: Readonly<{ projectId: string;
className={`grid grid-cols-12 gap-x-3 gap-y-2 p-4 ${layout === "compact" ? "gap-y-1" : ""}`}
data-legacy-form-layout={layout}
>
{elements.map((element) => (
<LegacyFormControl key={element.id} element={element} forceHalf={layout === "columns"} />
))}
{elements.length ? (
elements.map((element) => (
<LegacyFormControl key={element.id} element={element} forceHalf={layout === "columns"} />
))
) : (
<div className="col-span-12 border border-dashed border-[#aeb8c6] bg-white px-4 py-6 text-sm text-muted-foreground">
<div className="font-semibold text-foreground">Структура элементов формы не загружена</div>
<div className="mt-1 text-xs">{formQualifiedName(object, activeForm)}</div>
<div className="mt-3 text-xs">SFERA не подставляет реквизиты объекта вместо элементов формы, чтобы не искажать объект 1С.</div>
</div>
)}
</div>
</div>
</div>
@@ -5763,29 +5771,48 @@ function legacyFormControlInput(element: FormDesignerElementDraft): ReactNode {
if (element.kind === "text") {
return <textarea value={element.binding} readOnly className="min-h-16 resize-none border border-[#aeb8c6] bg-white px-2 py-1 text-xs" />;
}
return <input value={element.kind === "date" ? "21.05.2026 0:00:00" : element.binding} readOnly className="h-7 border border-[#aeb8c6] bg-white px-2 text-xs" />;
return <input value={element.binding} readOnly className="h-7 border border-[#aeb8c6] bg-white px-2 text-xs" />;
}
function buildFormDesignerElements(object: NormalizedMetadataObject, form: ObjectPart): FormDesignerElementDraft[] {
const fields = [...object.attributes, ...object.tabular_sections];
if (fields.length) {
return fields.map((field, index) => ({
id: `${form.name}-${field.kind}-${field.name}`,
name: field.name,
caption: field.name,
kind: field.kind === "TABULAR_SECTION" ? "table" : field.name.toLowerCase().includes("дата") ? "date" : "input",
binding: field.kind === "TABULAR_SECTION" ? `Объект.${field.name}` : field.name,
width: field.kind === "TABULAR_SECTION" ? "stretch" : index < 2 ? "half" : "stretch"
}));
function buildFormDesignerElements(form: ObjectPart): FormDesignerElementDraft[] {
const rawElements = form.attributes.elements;
if (Array.isArray(rawElements)) {
return rawElements
.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null)
.map((item, index) => {
const name = String(item.name ?? item.caption ?? `Элемент${index + 1}`);
const kind = String(item.control_kind ?? item.control ?? item.type ?? item.kind ?? "");
return {
id: String(item.lineage_id ?? item.id ?? `${form.name}-${name}-${index}`),
name,
caption: String(item.caption ?? name),
kind: formDesignerKindFor(kind, name),
binding: String(item.binding ?? item.path ?? name),
width: String(item.width ?? "") === "half" ? "half" : String(item.width ?? "") === "third" ? "third" : "stretch"
};
});
}
if (form.name.includes("ФормаДокумента")) {
return [
{ id: `${form.name}-number`, name: "Номер", caption: "Номер", kind: "input", binding: "Объект.Номер", width: "half" },
{ id: `${form.name}-date`, name: "Дата", caption: "Дата", kind: "date", binding: "Объект.Дата", width: "half" },
{ id: `${form.name}-table`, name: "Товары", caption: "Товары", kind: "table", binding: "Объект.Товары", width: "stretch" }
];
return [];
}
function formDesignerKindFor(kind: string, name: string): FormDesignerElementDraft["kind"] {
const raw = `${kind} ${name}`.toLowerCase();
if (raw.includes("table") || raw.includes("табли") || raw.includes("список")) {
return "table";
}
return [{ id: `${form.name}-name`, name: "Наименование", caption: "Наименование", kind: "input", binding: "Объект.Наименование", width: "stretch" }];
if (raw.includes("check") || raw.includes("boolean") || raw.includes("флаж") || raw.includes("булево")) {
return "checkbox";
}
if (raw.includes("date") || raw.includes("дата")) {
return "date";
}
if (raw.includes("group") || raw.includes("груп")) {
return "group";
}
if (raw.includes("text") || raw.includes("надпись")) {
return "text";
}
return "input";
}
function formQualifiedName(object: NormalizedMetadataObject, form: ObjectPart): string {