diff --git a/services/api-server/src/api_server/form_editor_models.py b/services/api-server/src/api_server/form_editor_models.py new file mode 100644 index 0000000..fcd6d22 --- /dev/null +++ b/services/api-server/src/api_server/form_editor_models.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass(slots=True) +class FormEditorCommand: + lineage_id: str + name: str + caption: str + handler_name: str + + +@dataclass(slots=True) +class FormEditorElement: + lineage_id: str + name: str + qualified_name: str + caption: str + control_kind: str + binding: str + width: str = "stretch" + row: int = 0 + + +@dataclass(slots=True) +class FormEditorDraft: + form_lineage_id: str + form_name: str + form_title: str + owner_name: str + layout_kind: str = "auto" + commands: list[FormEditorCommand] = field(default_factory=list) + elements: list[FormEditorElement] = field(default_factory=list) diff --git a/services/api-server/src/api_server/form_editor_view_model.py b/services/api-server/src/api_server/form_editor_view_model.py new file mode 100644 index 0000000..30e63e9 --- /dev/null +++ b/services/api-server/src/api_server/form_editor_view_model.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from api_server.form_editor_models import FormEditorCommand, FormEditorDraft, FormEditorElement + + +def build_form_editor_draft( + form_semantics: object, + form_module: object | None, + form_data: dict[str, list[str]] | None = None, +) -> FormEditorDraft: + form = getattr(form_semantics, "form", None) + form_name = str(getattr(form, "qualified_name", None) or getattr(form, "name", "Форма")) + module_attrs = getattr(form_module, "attributes", {}) or {} + owner_name = str(module_attrs.get("owner_qualified_name") or _owner_name_from_form(form_name)) + draft = FormEditorDraft( + form_lineage_id=str(getattr(form, "lineage_id", "") or ""), + form_name=form_name, + form_title=str(_first_value(form_data, "form_title") or getattr(form, "name", None) or "Форма"), + owner_name=owner_name, + layout_kind=str(_first_value(form_data, "layout_kind") or "auto"), + commands=_commands_from_semantics(form_semantics), + elements=_elements_from_semantics(form_semantics), + ) + if not draft.elements: + draft.elements = _default_elements_for_form(draft) + if form_data: + _apply_form_data(draft, form_data) + return draft + + +def _commands_from_semantics(form_semantics: object) -> list[FormEditorCommand]: + handlers = getattr(form_semantics, "command_handlers", {}) or {} + commands: list[FormEditorCommand] = [] + for command in getattr(form_semantics, "commands", []) or []: + lineage_id = str(getattr(command, "lineage_id", "") or "") + handler = handlers.get(lineage_id) + handler_name = str(getattr(handler, "name", None) or getattr(handler, "qualified_name", None) or "") + name = str(getattr(command, "name", "Команда")) + commands.append(FormEditorCommand(lineage_id=lineage_id, name=name, caption=name, handler_name=handler_name)) + return commands + + +def _elements_from_semantics(form_semantics: object) -> list[FormEditorElement]: + elements: list[FormEditorElement] = [] + for index, element in enumerate(getattr(form_semantics, "elements", []) or []): + attributes = getattr(element, "attributes", {}) or {} + name = str(getattr(element, "name", None) or getattr(element, "qualified_name", "Элемент")) + elements.append( + FormEditorElement( + lineage_id=str(getattr(element, "lineage_id", "") or f"element.{index}"), + name=name, + qualified_name=str(getattr(element, "qualified_name", name)), + caption=str(attributes.get("caption") or attributes.get("synonym") or name), + control_kind=_control_kind_for(name, attributes), + binding=str(attributes.get("path") or attributes.get("dataPath") or attributes.get("binding") or name), + width=str(attributes.get("width") or "stretch"), + row=index, + ) + ) + return elements + + +def _apply_form_data(draft: FormEditorDraft, form_data: dict[str, list[str]]) -> None: + captions = _values(form_data, "element_caption") + kinds = _values(form_data, "element_kind") + bindings = _values(form_data, "element_binding") + widths = _values(form_data, "element_width") + for index, element in enumerate(draft.elements): + element.caption = _at(captions, index, element.caption) + element.control_kind = _safe_control_kind(_at(kinds, index, element.control_kind)) + element.binding = _at(bindings, index, element.binding) + element.width = _safe_width(_at(widths, index, element.width)) + + command_captions = _values(form_data, "command_caption") + for index, command in enumerate(draft.commands): + command.caption = _at(command_captions, index, command.caption) + + new_name = str(_first_value(form_data, "new_element_name") or "").strip() + if new_name: + draft.elements.append( + FormEditorElement( + lineage_id=f"draft.{len(draft.elements) + 1}", + name=new_name, + qualified_name=f"{draft.form_name}.{new_name}", + caption=new_name, + control_kind=_safe_control_kind(_first_value(form_data, "new_element_kind") or "input"), + binding=new_name, + row=len(draft.elements), + ) + ) + + +def _default_elements_for_form(draft: FormEditorDraft) -> list[FormEditorElement]: + if "ФормаСписка" in draft.form_name: + return [ + FormEditorElement("default.list", "Список", f"{draft.form_name}.Список", "Список", "table", "Список", row=0), + ] + if "ФормаДокумента" in draft.form_name: + return [ + FormEditorElement("default.number", "Номер", f"{draft.form_name}.Номер", "Номер", "input", "Объект.Номер", "half", 0), + FormEditorElement("default.date", "Дата", f"{draft.form_name}.Дата", "Дата", "date", "Объект.Дата", "half", 1), + FormEditorElement("default.table", "Товары", f"{draft.form_name}.Товары", "Товары", "table", "Объект.Товары", row=2), + ] + return [ + FormEditorElement("default.name", "Наименование", f"{draft.form_name}.Наименование", "Наименование", "input", "Объект.Наименование", row=0), + ] + + +def _control_kind_for(name: str, attributes: dict) -> str: + raw = str(attributes.get("control") or attributes.get("control_kind") or attributes.get("type") or "").casefold() + if "table" in raw or "табли" in raw: + return "table" + if "checkbox" in raw or "boolean" in raw or "булево" in raw or name.casefold().startswith(("это", "пометка")): + return "checkbox" + if "date" in raw or "дата" in raw or "Дата" in name: + return "date" + if "group" in raw or "группа" in raw: + return "group" + return "input" + + +def _safe_control_kind(value: str) -> str: + return value if value in {"input", "date", "checkbox", "table", "group", "text"} else "input" + + +def _safe_width(value: str) -> str: + return value if value in {"stretch", "half", "third"} else "stretch" + + +def _owner_name_from_form(form_name: str) -> str: + parts = form_name.split(".") + if len(parts) >= 2: + return ".".join(parts[:2]) + return form_name + + +def _values(form_data: dict[str, list[str]], key: str) -> list[str]: + return [str(value) for value in form_data.get(key, [])] + + +def _first_value(form_data: dict[str, list[str]] | None, key: str) -> str | None: + if not form_data: + return None + values = form_data.get(key) + return str(values[0]) if values else None + + +def _at(values: list[str], index: int, default: str) -> str: + value = values[index].strip() if index < len(values) else "" + return value or default diff --git a/services/api-server/src/api_server/html5_editor_controller.py b/services/api-server/src/api_server/html5_editor_controller.py index 19eea2f..021af22 100644 --- a/services/api-server/src/api_server/html5_editor_controller.py +++ b/services/api-server/src/api_server/html5_editor_controller.py @@ -6,8 +6,9 @@ from typing import Any from fastapi import HTTPException from api_server.form_editor_service import form_module_for_form, select_form_semantics +from api_server.form_editor_view_model import build_form_editor_draft from api_server.html5_editor import render_html5_editor, render_html5_source, render_html5_symbol_detail -from api_server.html5_form_editor import render_html5_form_editor +from api_server.html5_form_editor import render_html5_form_designer, render_html5_form_editor from api_server.html5_inspector import ( render_html5_flowchart, render_html5_object_context, @@ -74,6 +75,28 @@ def html5_form_editor_page( ) +def html5_form_editor_preview( + *, + project_id: str, + form: dict[str, list[str]], + project_snapshot: Callable[[str], SirSnapshot], + form_semantics_items: Callable[[SirSnapshot], Iterable[object]], + form_semantics_response: Callable[[object], Any], +) -> str: + snapshot = project_snapshot(project_id) + form_id = _form_value(form, "form") + forms = [form_semantics_response(item) for item in form_semantics_items(snapshot)] + selected = select_form_semantics(forms, form_id) + form_module = form_module_for_form(snapshot, selected.form if selected is not None else None) + draft = build_form_editor_draft(selected, form_module, form) if selected is not None else None + return render_html5_form_designer(project_id, selected, form_module, draft=draft) + + +def _form_value(form: dict[str, list[str]], key: str) -> str | None: + values = form.get(key) + return str(values[0]) if values else None + + async def html5_object_context_fragment( *, project_id: str, diff --git a/services/api-server/src/api_server/html5_form_editor.py b/services/api-server/src/api_server/html5_form_editor.py index 9d488c0..5527cf0 100644 --- a/services/api-server/src/api_server/html5_form_editor.py +++ b/services/api-server/src/api_server/html5_form_editor.py @@ -4,6 +4,8 @@ from html import escape from typing import Iterable from urllib.parse import quote +from api_server.form_editor_models import FormEditorCommand, FormEditorDraft, FormEditorElement +from api_server.form_editor_view_model import build_form_editor_draft from api_server.html5 import _metric, _page, _project_link, _topbar from api_server.html5_editor import render_html5_source from sir import SirSnapshot @@ -42,6 +44,7 @@ def render_html5_form_editor( handlers = getattr(selected, "command_handlers", {}) if selected is not None else {} module_lineage = str(getattr(form_module, "lineage_id", "") or "") form_name = getattr(selected_form, "qualified_name", None) or getattr(selected_form, "name", "Форма") + draft = build_form_editor_draft(selected, form_module) if selected is not None else None content = f"""
- {_form_designer_surface(project_id, selected, form_module)} + {render_html5_form_designer(project_id, selected, form_module, draft=draft)}