Update legacy form designer UI
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-21 05:08:03 +03:00
parent e546985843
commit ebded03ecf
@@ -5500,7 +5500,7 @@ function ObjectWorkbench({
<ObjectPartList title="Модули" rows={object.modules} />
<ObjectRightsList rows={object.rights} />
</div>
<FormDesignerPanel object={object} />
<FormDesignerPanel projectId={projectId} object={object} />
<ObjectImpactPanel impact={objectImpact} />
<ModuleSourcePanel
projectId={projectId}
@@ -5512,18 +5512,75 @@ function ObjectWorkbench({
);
}
function FormDesignerPanel({ object }: Readonly<{ object: NormalizedMetadataObject }>) {
type FormDesignerElementDraft = {
id: string;
name: string;
caption: string;
kind: "input" | "date" | "checkbox" | "table" | "group" | "text";
binding: string;
width: "stretch" | "half" | "third";
};
function FormDesignerPanel({ projectId, object }: Readonly<{ projectId: string; object: NormalizedMetadataObject }>) {
const [activeFormName, setActiveFormName] = useState<string | null>(null);
const activeForm = object.forms.find((form) => form.name === activeFormName) ?? object.forms[0] ?? null;
const formFields = [...object.attributes, ...object.tabular_sections];
const activeFormKey = activeForm?.name ?? "";
const [titleByForm, setTitleByForm] = useState<Record<string, string>>({});
const [layoutByForm, setLayoutByForm] = useState<Record<string, "auto" | "columns" | "compact">>({});
const [elementOverrides, setElementOverrides] = useState<Record<string, Partial<FormDesignerElementDraft>>>({});
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 elements = [...baseElements, ...(extraElementsByForm[activeFormKey] ?? [])].map((element) => ({
...element,
...(elementOverrides[element.id] ?? {})
}));
const formTitle = titleByForm[activeFormKey] ?? activeForm?.name ?? "Форма";
const layout = layoutByForm[activeFormKey] ?? "auto";
const html5EditorHref = activeForm
? `/html5/projects/${encodeURIComponent(projectId)}/forms/editor?form=${encodeURIComponent(formQualifiedName(object, activeForm))}`
: `/html5/projects/${encodeURIComponent(projectId)}/forms/editor`;
const updateElement = (id: string, patch: Partial<FormDesignerElementDraft>) => {
setElementOverrides((current) => ({ ...current, [id]: { ...(current[id] ?? {}), ...patch } }));
};
const addElement = () => {
const name = newElementName.trim();
if (!name || !activeFormKey) {
return;
}
const next: FormDesignerElementDraft = {
id: `draft-${activeFormKey}-${Date.now()}`,
name,
caption: name,
kind: newElementKind,
binding: name,
width: "stretch"
};
setExtraElementsByForm((current) => ({ ...current, [activeFormKey]: [...(current[activeFormKey] ?? []), next] }));
setNewElementName("");
};
return (
<div className="mt-5 rounded-md border border-border">
<div className="flex h-10 items-center justify-between border-b border-border px-3">
<div className="text-sm font-medium">Form designer</div>
<Badge>{object.forms.length}</Badge>
<div className="mt-5 overflow-hidden rounded-md border border-border">
<div className="flex min-h-10 items-center justify-between gap-3 border-b border-border px-3 py-2">
<div>
<div className="text-sm font-medium">Редактор формы</div>
<div className="text-xs text-muted-foreground">Управляемая форма 1С 8.5: элементы, команды, реквизиты и модуль остаются частью объекта.</div>
</div>
<div className="flex items-center gap-2">
{activeForm ? (
<a className="rounded-md border border-border px-2 py-1 text-xs font-medium hover:bg-muted" href={html5EditorHref}>
HTML5 редактор
</a>
) : null}
<Badge>{object.forms.length}</Badge>
</div>
</div>
{activeForm ? (
<div className="grid min-h-72 grid-cols-[220px_minmax(0,1fr)_260px]">
<div className="grid min-h-[520px] grid-cols-[220px_minmax(0,1fr)_340px]">
<div className="border-r border-border p-2">
{object.forms.map((form) => (
<button
@@ -5535,40 +5592,136 @@ function FormDesignerPanel({ object }: Readonly<{ object: NormalizedMetadataObje
}`}
>
<div className="truncate font-medium">{form.name}</div>
<div className="truncate text-xs text-muted-foreground">{form.kind}</div>
<div className="truncate text-xs text-muted-foreground">{formQualifiedName(object, form)}</div>
</button>
))}
</div>
<div className="min-w-0 p-3">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-semibold">{activeForm.name}</div>
<div className="truncate text-xs text-muted-foreground">{object.qualified_name}</div>
<div className="min-w-0 bg-[#f3f5f8] p-5">
<div className="mx-auto min-h-[430px] max-w-[760px] border border-[#b8c0ca] bg-[#fbfbfc] shadow-lg" data-legacy-form-window>
<div className="flex min-h-9 items-center justify-between border-b border-[#cbd3df] bg-gradient-to-b from-[#f8f9fb] to-[#e9edf3] px-3 text-sm font-semibold">
<span className="truncate">{formTitle}</span>
<span className="text-[11px] text-muted-foreground">1C:Enterprise 8.5-style managed form</span>
</div>
<div className="flex min-h-11 flex-wrap gap-1 border-b border-[#ccd4df] bg-[#f4f6f9] px-3 py-2">
{object.commands.length ? object.commands.map((command) => (
<button
key={`form-command-${command.name}`}
type="button"
className="h-7 rounded-none border border-[#aeb8c6] bg-gradient-to-b from-white to-[#edf1f6] px-3 text-xs font-medium text-[#1f2937]"
>
{command.name}
</button>
)) : <span className="text-xs text-muted-foreground">Команды не описаны</span>}
</div>
<div
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"} />
))}
</div>
<Badge>{activeForm.kind}</Badge>
</div>
<div className="grid gap-2 md:grid-cols-2 xl:grid-cols-3">
{formFields.slice(0, 18).map((field) => (
<div key={`field-${field.kind}-${field.name}`} className="rounded-md border border-border p-2">
<div className="truncate text-sm font-medium">{field.name}</div>
<div className="truncate text-xs text-muted-foreground">{field.kind}</div>
</div>
))}
{formFields.length === 0 ? (
<div className="text-sm text-muted-foreground">Поля формы не найдены в structure-only metadata.</div>
) : null}
</div>
</div>
<div className="border-l border-border p-3">
<div className="text-sm font-medium">Form context</div>
<div className="mt-3 grid grid-cols-2 gap-2">
<SmallMetric label="fields" value={formFields.length} />
<div className="overflow-auto border-l border-border bg-background">
<div className="border-b border-border p-3">
<div className="text-sm font-medium">Свойства формы</div>
<div className="mt-3 grid gap-2">
<label className="grid gap-1 text-xs font-medium text-muted-foreground">
Заголовок
<input
value={formTitle}
onChange={(event) => setTitleByForm((current) => ({ ...current, [activeFormKey]: event.target.value }))}
className="h-8 border border-border bg-background px-2 text-sm text-foreground"
/>
</label>
<label className="grid gap-1 text-xs font-medium text-muted-foreground">
Размещение
<select
value={layout}
onChange={(event) => setLayoutByForm((current) => ({ ...current, [activeFormKey]: event.target.value as "auto" | "columns" | "compact" }))}
className="h-8 border border-border bg-background px-2 text-sm text-foreground"
>
<option value="auto">Авто 1С</option>
<option value="columns">Колонки</option>
<option value="compact">Компактно</option>
</select>
</label>
</div>
</div>
<div className="grid grid-cols-2 gap-2 border-b border-border p-3">
<SmallMetric label="elements" value={elements.length} />
<SmallMetric label="commands" value={object.commands.length} />
<SmallMetric label="modules" value={object.modules.length} />
<SmallMetric label="events" value={object.events.length} />
</div>
<ObjectMetaList title="Commands" rows={object.commands} />
<ObjectMetaList title="Form metadata" rows={Object.entries(activeForm.attributes ?? {}).map(([key, value]) => `${key}: ${String(value)}`)} />
<div className="border-b border-border p-3">
<div className="text-xs font-medium text-muted-foreground">Добавить элемент</div>
<div className="mt-2 grid grid-cols-[minmax(0,1fr)_110px] gap-2">
<input
value={newElementName}
onChange={(event) => setNewElementName(event.target.value)}
placeholder="Новый реквизит"
className="h-8 border border-border bg-background px-2 text-sm text-foreground"
/>
<select
value={newElementKind}
onChange={(event) => setNewElementKind(event.target.value as FormDesignerElementDraft["kind"])}
className="h-8 border border-border bg-background px-2 text-sm text-foreground"
>
<option value="input">Поле</option>
<option value="date">Дата</option>
<option value="checkbox">Флажок</option>
<option value="table">Таблица</option>
<option value="group">Группа</option>
<option value="text">Текст</option>
</select>
</div>
<button type="button" onClick={addElement} className="mt-2 h-8 w-full rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground">
Добавить в макет
</button>
</div>
<div className="divide-y divide-border">
{elements.map((element) => (
<div key={`props-${element.id}`} className="grid gap-2 p-3">
<div className="truncate text-xs font-semibold">{element.name}</div>
<input
value={element.caption}
onChange={(event) => updateElement(element.id, { caption: event.target.value })}
className="h-8 border border-border bg-background px-2 text-sm text-foreground"
/>
<div className="grid grid-cols-2 gap-2">
<select
value={element.kind}
onChange={(event) => updateElement(element.id, { kind: event.target.value as FormDesignerElementDraft["kind"] })}
className="h-8 border border-border bg-background px-2 text-sm text-foreground"
>
<option value="input">Поле ввода</option>
<option value="date">Дата</option>
<option value="checkbox">Флажок</option>
<option value="table">Таблица</option>
<option value="group">Группа</option>
<option value="text">Текст</option>
</select>
<select
value={element.width}
onChange={(event) => updateElement(element.id, { width: event.target.value as FormDesignerElementDraft["width"] })}
className="h-8 border border-border bg-background px-2 text-sm text-foreground"
>
<option value="stretch">Вся строка</option>
<option value="half">1/2</option>
<option value="third">1/3</option>
</select>
</div>
<input
value={element.binding}
onChange={(event) => updateElement(element.id, { binding: event.target.value })}
className="h-8 border border-border bg-background px-2 text-sm text-foreground"
/>
</div>
))}
</div>
<ObjectMetaList title="Модуль/метаданные формы" rows={Object.entries(activeForm.attributes ?? {}).map(([key, value]) => `${key}: ${String(value)}`)} />
</div>
</div>
) : (
@@ -5578,6 +5731,68 @@ function FormDesignerPanel({ object }: Readonly<{ object: NormalizedMetadataObje
);
}
function LegacyFormControl({ element, forceHalf }: Readonly<{ element: FormDesignerElementDraft; forceHalf: boolean }>) {
const span = element.kind === "table" || element.kind === "group" ? "col-span-12" : forceHalf || element.width === "half" ? "col-span-6" : element.width === "third" ? "col-span-4" : "col-span-12";
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`}>
<label className="truncate text-xs font-semibold text-[#4b5563]">{element.caption}</label>
{legacyFormControlInput(element)}
</div>
);
}
function legacyFormControlInput(element: FormDesignerElementDraft): ReactNode {
if (element.kind === "table") {
return (
<div className="min-h-24 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>
<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>
);
}
if (element.kind === "checkbox") {
return <label className="flex items-center gap-2 text-xs"><input type="checkbox" checked readOnly />{element.binding}</label>;
}
if (element.kind === "group") {
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.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" />;
}
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"
}));
}
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 [{ id: `${form.name}-name`, name: "Наименование", caption: "Наименование", kind: "input", binding: "Объект.Наименование", width: "stretch" }];
}
function formQualifiedName(object: NormalizedMetadataObject, form: ObjectPart): string {
const qualified = form.attributes.qualified_name ?? form.attributes.qualifiedName;
return typeof qualified === "string" && qualified ? qualified : `${object.qualified_name}.${form.name}`;
}
function ObjectMetaList({ title, rows }: Readonly<{ title: string; rows: ObjectPart[] | string[] }>) {
return (
<div className="mt-3">