Update legacy form designer UI
This commit is contained in:
@@ -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>
|
||||
<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>
|
||||
<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 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"} />
|
||||
))}
|
||||
{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>
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user