Replace IDE form mode placeholder
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-21 05:15:43 +03:00
parent ebded03ecf
commit d26aaef44a
@@ -3036,22 +3036,59 @@ function FormDesignerPanel({
const t = messages[language];
const objectForms = data.selectedObjectUi?.forms.length ? data.selectedObjectUi.forms : data.forms;
const [selectedFormId, setSelectedFormId] = useState<string | null>(objectForms[0]?.form.lineage_id ?? null);
const [titleByForm, setTitleByForm] = useState<Record<string, string>>({});
const [layoutByForm, setLayoutByForm] = useState<Record<string, "auto" | "columns" | "compact">>({});
const [elementDrafts, setElementDrafts] = useState<Record<string, IdeFormElementDraft[]>>({});
const [newElementName, setNewElementName] = useState("");
const [newElementKind, setNewElementKind] = useState<IdeFormElementDraft["controlKind"]>("input");
useEffect(() => {
if (objectForms.length > 0 && !objectForms.some((item) => item.form.lineage_id === selectedFormId)) {
setSelectedFormId(objectForms[0].form.lineage_id);
}
}, [objectForms, selectedFormId]);
const form = objectForms.find((item) => item.form.lineage_id === selectedFormId) ?? objectForms[0];
const elements = form?.elements.slice(0, 8) ?? [];
const commands = form?.commands.slice(0, 6) ?? [];
const formTitle = form?.form.name ?? "ФормаДокумента";
const formObjectCaption = language === "ru" ? `${formTitle} (форма 1С)` : `${formTitle} (1C form)`;
const commandLabels = [t.postAndClose, ...commands.slice(0, 2).map((command) => command.name)];
const formKey = form?.form.lineage_id ?? "draft";
const baseElements = useMemo(() => buildIdeFormElements(data, form), [data, 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)`;
const layout = layoutByForm[formKey] ?? "auto";
useEffect(() => {
if (form && !elementDrafts[form.form.lineage_id]) {
setElementDrafts((current) => ({ ...current, [form.form.lineage_id]: buildIdeFormElements(data, form) }));
}
}, [data, elementDrafts, form]);
const updateElement = (id: string, patch: Partial<IdeFormElementDraft>) => {
setElementDrafts((current) => ({
...current,
[formKey]: (current[formKey] ?? baseElements).map((element) => (element.id === id ? { ...element, ...patch } : element))
}));
};
const addElement = () => {
const name = newElementName.trim();
if (!name) {
return;
}
const next: IdeFormElementDraft = {
id: `draft.${formKey}.${Date.now()}`,
name,
caption: name,
controlKind: newElementKind,
binding: name,
width: "stretch"
};
setElementDrafts((current) => ({ ...current, [formKey]: [...(current[formKey] ?? baseElements), next] }));
setNewElementName("");
};
return (
<Card className="min-h-0 rounded-none border-0 p-0 shadow-none" data-active-mode={modeId}>
<PanelTitle icon={Layers3} title={t.formDesigner} />
<div className="grid h-[calc(100%-45px)] min-h-0 grid-cols-[260px_minmax(0,1fr)]">
<div className="grid h-[calc(100%-45px)] min-h-0 grid-cols-[260px_minmax(0,1fr)_340px]">
<div className="min-h-0 overflow-auto border-r border-border bg-[#f4f4f4] p-3">
<div className="flex items-center justify-between border-b border-border pb-2">
<div className="text-xs font-semibold uppercase text-muted-foreground">{t.forms}</div>
@@ -3086,11 +3123,11 @@ function FormDesignerPanel({
<div className="text-sm text-muted-foreground">{t.none}</div>
) : (
elements.map((element) => (
<div className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-background" key={element.lineage_id}>
<OneCTreeIcon kind={kindForTreeLabel(element.kind)} />
<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">
<div className="truncate font-medium text-foreground">{element.name}</div>
<div className="truncate text-[11px] text-muted-foreground">{element.kind}</div>
<div className="truncate text-[11px] text-muted-foreground">{element.controlKind} · {element.binding}</div>
</div>
</div>
))
@@ -3107,146 +3144,234 @@ function FormDesignerPanel({
</div>
</div>
<div className="min-h-0 overflow-auto bg-[#ececec] p-5">
<div className="mx-auto flex min-h-[760px] max-w-7xl flex-col rounded-xl border border-black/5 bg-white px-5 py-5 shadow-[0_18px_55px_rgba(15,23,42,0.18)]">
<div className="flex items-start justify-between">
<div className="mx-auto flex min-h-[760px] max-w-5xl flex-col border border-[#b8c0ca] bg-[#fbfbfc] shadow-[0_18px_55px_rgba(15,23,42,0.18)]" data-ide-form-window data-ide-form-layout={layout}>
<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 text-slate-950">
<div>
<div className="text-lg font-medium text-slate-950">{formObjectCaption}</div>
<button className="mt-8 inline-flex items-center gap-1 text-sm text-slate-900" type="button">
{t.mainSection}
<ChevronDown className="h-3.5 w-3.5" />
</button>
<div className="font-semibold">{formObjectCaption}</div>
<div className="text-[11px] text-slate-500">{data.selectedObjectSchema?.object.qualified_name ?? form?.form.qualified_name ?? data.projectId}</div>
</div>
<div className="flex items-center gap-2 text-slate-500">
<button className="flex h-8 w-8 items-center justify-center rounded-full bg-slate-100 hover:bg-slate-200" type="button" title="menu">
<button className="flex h-7 w-7 items-center justify-center rounded-full bg-slate-100 hover:bg-slate-200" type="button" title="menu">
<MoreVertical className="h-4 w-4" />
</button>
<button className="flex h-8 w-8 items-center justify-center rounded-full bg-slate-100 hover:bg-slate-200" type="button" title="expand">
<button className="flex h-7 w-7 items-center justify-center rounded-full bg-slate-100 hover:bg-slate-200" type="button" title="expand">
<Maximize2 className="h-4 w-4" />
</button>
<button className="flex h-8 w-8 items-center justify-center rounded-full bg-slate-100 hover:bg-slate-200" type="button" title="close">
<button className="flex h-7 w-7 items-center justify-center rounded-full bg-slate-100 hover:bg-slate-200" type="button" title="close">
<X className="h-4 w-4" />
</button>
</div>
</div>
<div className="mt-7 grid max-w-3xl grid-cols-[minmax(0,360px)_minmax(0,360px)] gap-x-3 gap-y-3">
<label className="grid gap-1 text-xs text-slate-700">
<span><span className="text-red-500">*</span>{t.nameField}</span>
<span className="flex h-8 items-center rounded-md border border-slate-300 bg-white px-2 text-sm text-slate-900">{formTitle}</span>
</label>
<label className="grid gap-1 text-xs text-slate-700">
{t.code}
<span className="flex h-8 items-center gap-2 rounded-md border border-slate-300 bg-slate-100 px-2 text-sm text-slate-700">
X-00044
<Lock className="ml-auto h-3.5 w-3.5 text-slate-400" />
</span>
</label>
<div className="flex items-center gap-4 pt-5 text-sm text-slate-900">
{[t.client, t.agent, t.supplier].map((label, index) => (
<label className="inline-flex items-center gap-2" key={label}>
<span className={index === 1 ? "flex h-5 w-5 items-center justify-center rounded border border-blue-500 bg-white text-blue-600" : "h-5 w-5 rounded border border-slate-400 bg-white"}>
{index === 1 ? "✓" : ""}
</span>
{label}
</label>
))}
</div>
<label className="grid gap-1 text-xs text-slate-700">
{t.status}
<span className="relative flex h-8 items-center rounded-md border border-blue-600 bg-white px-2 text-sm">
<span className="h-4 w-px bg-slate-950" />
<ChevronDown className="ml-auto h-4 w-4 text-blue-600" />
<span className="absolute left-0 top-9 z-10 grid w-32 gap-1 rounded-lg bg-[#3f3f3f] p-3 text-xs text-white shadow-xl">
<span>{t.agent}</span>
<span>{language === "ru" ? "Прямой клиент" : "Direct client"}</span>
</span>
</span>
</label>
<label className="grid gap-1 text-xs text-slate-700">
{t.comment}
<span className="h-8 rounded-md border border-slate-300 bg-white" />
</label>
</div>
<div className="mt-4 border-b border-slate-200">
<div className="flex gap-5 text-sm">
{[t.goods, t.sites, t.compensationTerms, t.agencyAgreements, t.telegram, t.mail].map((tab, index) => (
<button
className={index === 0 ? "border-b-2 border-yellow-400 px-2 py-2 text-slate-950" : "px-2 py-2 text-slate-500 hover:text-slate-900"}
key={tab}
type="button"
>
{tab}
</button>
))}
</div>
</div>
<div className="mt-3 overflow-hidden rounded-md border border-slate-100">
<div className="flex h-10 items-center gap-3 bg-[#f0f0f0] px-3 text-sm text-slate-800">
<button className="inline-flex h-8 items-center gap-2 rounded px-2 hover:bg-white" type="button">
<Plus className="h-4 w-4" />
{t.create}
<div className="flex min-h-11 flex-wrap items-center gap-1 border-b border-[#ccd4df] bg-[#f4f6f9] px-3 py-2">
{commands.length ? commands.map((command) => (
<button className="h-7 border border-[#aeb8c6] bg-gradient-to-b from-white to-[#edf1f6] px-3 text-xs font-medium text-[#1f2937]" key={command.lineage_id} type="button">
{command.name}
</button>
<span className="h-6 w-px bg-slate-300" />
<button className="flex h-8 w-8 items-center justify-center rounded hover:bg-white" type="button" title="copy">
<Copy className="h-4 w-4 text-slate-500" />
</button>
<div className="ml-auto flex items-center gap-3">
<button className="inline-flex items-center gap-2 hover:text-slate-950" type="button">
<Search className="h-4 w-4" />
{t.search}
</button>
<button className="flex h-8 w-8 items-center justify-center rounded hover:bg-white" type="button" title="filter">
<Filter className="h-4 w-4" />
</button>
<button className="flex h-8 w-8 items-center justify-center rounded hover:bg-white" type="button" title="more">
<MoreVertical className="h-4 w-4" />
</button>
</div>
</div>
<div className="grid grid-cols-[48px_1.1fr_90px_1.6fr_1fr_1fr_1.2fr] border-b border-slate-100 text-xs font-semibold text-slate-950">
{["", t.nameField, t.code, t.sentToBankCompanyName, t.mergeProject, t.legalEntity, t.result].map((heading) => (
<div className="px-3 py-3" key={heading}>{heading}</div>
))}
</div>
<div className="flex min-h-[210px] items-center justify-center text-sm text-slate-500">{t.emptyList}</div>
)) : <span className="text-xs text-muted-foreground">{language === "ru" ? "Команды формы не описаны" : "No form commands"}</span>}
</div>
<div className="mt-auto grid max-w-3xl grid-cols-[minmax(0,330px)_minmax(0,360px)] gap-x-11 gap-y-3 pt-6">
{[
[t.author, t.none],
[t.creationDate, t.none],
[t.editor, t.none],
[t.editDate, t.none]
].map(([label, value]) => (
<label className="grid gap-1 text-xs text-slate-700" key={label}>
{label}
<span className="flex h-8 items-center rounded-md border border-slate-300 bg-slate-100 px-2 text-sm text-slate-700">{value}</span>
</label>
<div className={[
"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} />
))}
</div>
<div className="mt-5 flex items-center justify-end gap-3">
{commandLabels.map((command) => (
<button className="h-8 rounded-md bg-slate-100 px-3 text-xs font-medium text-slate-700 hover:bg-slate-200" key={command} type="button">
{command}
<div className="mt-auto border-t border-[#ccd4df] bg-white px-4 py-3">
<div className="flex flex-wrap justify-end gap-2">
{commands.slice(0, 3).map((command) => (
<button className="h-8 border border-[#aeb8c6] bg-gradient-to-b from-white to-[#edf1f6] px-3 text-xs font-medium text-slate-700" key={`bottom-${command.lineage_id}`} type="button">
{command.name}
</button>
))}
<button className="h-8 bg-yellow-400 px-4 text-sm font-semibold text-slate-950 hover:bg-yellow-300" type="button">
{t.saveAndClose}
</button>
))}
<button className="h-9 rounded-md bg-yellow-400 px-4 text-sm font-semibold text-slate-950 hover:bg-yellow-300" type="button">
{t.saveAndClose}
</button>
<button className="h-9 rounded-md bg-slate-200 px-4 text-sm font-semibold text-slate-900 hover:bg-slate-300" type="button">
{t.save}
</button>
<button className="h-8 bg-slate-200 px-4 text-sm font-semibold text-slate-900 hover:bg-slate-300" type="button">
{t.save}
</button>
</div>
</div>
</div>
</div>
<aside className="min-h-0 overflow-auto border-l border-border bg-background">
<div className="border-b border-border p-3">
<div className="text-sm font-medium">{language === "ru" ? "Свойства формы" : "Form properties"}</div>
<label className="mt-3 grid gap-1 text-xs font-medium text-muted-foreground">
{language === "ru" ? "Заголовок" : "Title"}
<input className="h-8 border border-border bg-background px-2 text-sm text-foreground" value={formTitle} onChange={(event) => setTitleByForm((current) => ({ ...current, [formKey]: event.target.value }))} />
</label>
<label className="mt-2 grid gap-1 text-xs font-medium text-muted-foreground">
{language === "ru" ? "Размещение" : "Layout"}
<select className="h-8 border border-border bg-background px-2 text-sm text-foreground" value={layout} onChange={(event) => setLayoutByForm((current) => ({ ...current, [formKey]: event.target.value as "auto" | "columns" | "compact" }))}>
<option value="auto">{language === "ru" ? "Авто 1С" : "1C auto"}</option>
<option value="columns">{language === "ru" ? "Колонки" : "Columns"}</option>
<option value="compact">{language === "ru" ? "Компактно" : "Compact"}</option>
</select>
</label>
</div>
<div className="grid grid-cols-2 gap-2 border-b border-border p-3">
<IdeFormMetric label="elements" value={elements.length} />
<IdeFormMetric label="commands" value={commands.length} />
</div>
<div className="border-b border-border p-3">
<div className="text-xs font-medium text-muted-foreground">{language === "ru" ? "Добавить элемент" : "Add element"}</div>
<div className="mt-2 grid grid-cols-[minmax(0,1fr)_110px] gap-2">
<input className="h-8 border border-border bg-background px-2 text-sm text-foreground" placeholder={language === "ru" ? "Новый реквизит" : "New attribute"} value={newElementName} onChange={(event) => setNewElementName(event.target.value)} />
<select className="h-8 border border-border bg-background px-2 text-sm text-foreground" value={newElementKind} onChange={(event) => setNewElementKind(event.target.value as IdeFormElementDraft["controlKind"])}>
<option value="input">{language === "ru" ? "Поле" : "Input"}</option>
<option value="date">{language === "ru" ? "Дата" : "Date"}</option>
<option value="checkbox">{language === "ru" ? "Флажок" : "Checkbox"}</option>
<option value="table">{language === "ru" ? "Таблица" : "Table"}</option>
<option value="group">{language === "ru" ? "Группа" : "Group"}</option>
<option value="text">{language === "ru" ? "Текст" : "Text"}</option>
</select>
</div>
<button className="mt-2 h-8 w-full bg-primary px-3 text-sm font-medium text-primary-foreground" onClick={addElement} type="button">
{language === "ru" ? "Добавить в макет" : "Add to layout"}
</button>
</div>
<div className="divide-y divide-border">
{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 })} />
<div className="grid grid-cols-2 gap-2">
<select className="h-8 border border-border bg-background px-2 text-sm text-foreground" value={element.controlKind} onChange={(event) => updateElement(element.id, { controlKind: event.target.value as IdeFormElementDraft["controlKind"] })}>
<option value="input">{language === "ru" ? "Поле ввода" : "Input"}</option>
<option value="date">{language === "ru" ? "Дата" : "Date"}</option>
<option value="checkbox">{language === "ru" ? "Флажок" : "Checkbox"}</option>
<option value="table">{language === "ru" ? "Таблица" : "Table"}</option>
<option value="group">{language === "ru" ? "Группа" : "Group"}</option>
<option value="text">{language === "ru" ? "Текст" : "Text"}</option>
</select>
<select className="h-8 border border-border bg-background px-2 text-sm text-foreground" value={element.width} onChange={(event) => updateElement(element.id, { width: event.target.value as IdeFormElementDraft["width"] })}>
<option value="stretch">{language === "ru" ? "Вся строка" : "Full"}</option>
<option value="half">1/2</option>
<option value="third">1/3</option>
</select>
</div>
<input className="h-8 border border-border bg-background px-2 text-sm text-foreground" value={element.binding} onChange={(event) => updateElement(element.id, { binding: event.target.value })} />
</div>
))}
</div>
</aside>
</div>
</Card>
);
}
type IdeFormElementDraft = {
id: string;
name: string;
caption: string;
controlKind: "input" | "date" | "checkbox" | "table" | "group" | "text";
binding: string;
width: "stretch" | "half" | "third";
};
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";
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>
{ideFormControlInput(element)}
</div>
);
}
function ideFormControlInput(element: IdeFormElementDraft) {
if (element.controlKind === "table") {
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>
<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>
</div>
);
}
if (element.controlKind === "checkbox") {
return <label className="flex items-center gap-2 text-xs text-slate-800"><input checked readOnly type="checkbox" />{element.binding}</label>;
}
if (element.controlKind === "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.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} />;
}
function IdeFormMetric({ label, value }: Readonly<{ label: string; value: number }>) {
return (
<div className="border border-border p-2">
<div className="text-base font-semibold">{value}</div>
<div className="text-[11px] text-muted-foreground">{label}</div>
</div>
);
}
function buildIdeFormElements(data: ProjectWorkspaceData, 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: element.name,
controlKind: controlKindForFormNode(element.name, element.kind),
binding: element.qualified_name || element.name,
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" }];
}
function controlKindForFormNode(name: string, kind: string): IdeFormElementDraft["controlKind"] {
const normalized = `${name} ${kind}`.toLowerCase();
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";
return "input";
}
function MetadataDesignerPanel({
activeMode,
data,