"use client"; import { Bot, Brain as BrainIcon, Braces, CheckCircle2, ChevronDown, Code2, Copy, Database as DatabaseIcon, Filter, FlaskConical, FileText, FileCode2, GitCompareArrows, GraduationCap, History, Layers3, ListTree, Lock, Maximize2, Monitor, MoreVertical, PanelBottom, PanelRight, Pin, Plus, Search, Sparkles, TriangleAlert, X } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import type React from "react"; import { LazyMetadataTree } from "@/components/editor/lazy-metadata-tree"; import { Badge } from "@/components/ui/badge"; import { Card } from "@/components/ui/card"; import { applyAuthoringChangeSet, ensureAuthoringSession, getAuthoringSemanticDiffPreview, applyAuthoringMetadataObject, applyAuthoringRollback, getAuthoringMetadataObjectPreview, getBslCompletions, getLineageVersions, getProjectSymbolDefinition, getProjectFlowchart, getProjectSymbolReferences, getVersionDiff, getAuthoringRollbackPreview, searchProjectSymbols, type AuthoringMetadataObjectDraft, type BslCompletionItem, type MetadataTreeNode, type MetadataTypeSpec, type FlowchartNode, type ProjectFlowchart, type SymbolReferences, type SymbolResult, type getProjectWorkspaceData, resolveApiUrl, type SirNode } from "@/lib/api"; import { messages, type UiLanguage } from "@/lib/i18n"; type ProjectWorkspaceData = Awaited>; type AuthoringChange = ProjectWorkspaceData["authoringChanges"][number]; type ProjectVersion = ProjectWorkspaceData["projectVersions"][number]; type VersionDetail = Awaited>[number]; type VersionDiff = Awaited>; type GuardCheck = { name: string; status: string; message: string }; type WorkspaceMode = "overview" | "module" | "form" | "properties" | "events" | "versions" | "documentation" | "knowledge" | "learning" | "flowchart"; type OpenDocument = { id: string; mode: WorkspaceMode; label: string; meta: string; icon: React.ComponentType<{ className?: string }>; }; type OneCIconKind = | "attribute" | "business-process" | "catalog" | "command" | "common" | "constant" | "document" | "exchange-plan" | "event" | "enum" | "external-source" | "extension" | "form" | "integration" | "journal" | "language" | "layout" | "module" | "palette" | "plan" | "processing" | "register" | "report" | "role" | "scheduled-job" | "service" | "settings" | "subsystem" | "tabular" | "task" | "tree" | "web"; const oneCIconFiles: Record = { attribute: "attribute.svg", "business-process": "businessProcess.svg", catalog: "catalog.svg", command: "command.svg", common: "common.svg", constant: "constant.svg", document: "document.svg", event: "eventSubscription.svg", "exchange-plan": "exchangePlan.svg", enum: "enum.svg", "external-source": "externalDataSource.svg", extension: "folder.svg", form: "form.svg", integration: "ws.svg", journal: "documentJournal.svg", language: "style.svg", layout: "template.svg", module: "commonModule.svg", palette: "picture.svg", plan: "chartsOfAccount.svg", processing: "dataProcessor.svg", register: "accumulationRegister.svg", report: "report.svg", role: "role.svg", "scheduled-job": "scheduledJob.svg", service: "http.svg", settings: "parameter.svg", subsystem: "subsystem.svg", tabular: "tabularSection.svg", task: "task.svg", tree: "folder.svg", web: "http.svg" }; const sampleCode = [ "// Код модуля не загружен.", "// Выберите реальный модуль в дереве проекта или выполните индексирование с исходниками BSL." ]; const suggestedCode = [ "// AI-подсказка появится после выбора реального контекста." ]; const outlineItems = [ "Выберите модуль в дереве" ]; const knownObjectKinds = new Set([ "CATALOG", "DOCUMENT", "REGISTER", "COMMON_MODULE", "EXCHANGE_PLAN", "EVENT_SUBSCRIPTION", "SCHEDULED_JOB", "BUSINESS_PROCESS", "TASK", "FORM", "COMMAND", "ATTRIBUTE", "TABULAR_SECTION", "PROCEDURE", "FUNCTION" ]); function versionToSummary(version: { version_id: string; lineage_id: string; semantic_id: string; object_hash: string; parent_version_id?: string | null; task_id?: string | null; session_id?: string | null; }): ProjectVersion { return { version_id: version.version_id, lineage_id: version.lineage_id, semantic_id: version.semantic_id, object_hash: version.object_hash, parent_version_id: version.parent_version_id ?? null, task_id: version.task_id ?? null, session_id: version.session_id ?? null }; } export function IdeWorkspace({ data, initialMode, language, routineName }: Readonly<{ data: ProjectWorkspaceData; initialMode?: string; language: UiLanguage; routineName?: string; }>) { const t = messages[language]; const treeNodes = data.exportSnapshot?.nodes ?? []; const objectNodes = treeNodes.filter((node) => knownObjectKinds.has(node.kind)).slice(0, 14); const selectedObject = objectNodes.find((node) => node.lineage_id === data.authoringPreview?.context.object?.lineage_id) ?? objectNodes[0]; const selectedMetadataNode = data.selectedMetadataNode ?? findMetadataTreeNode(data.metadataTree?.root, routineName) ?? flattenMetadataTree(data.metadataTree?.root, 1)[0]; const problems = data.review.slice(0, 3); const [activeMode, setActiveMode] = useState(() => normalizeWorkspaceMode(initialMode)); const [authoringChanges, setAuthoringChanges] = useState(data.authoringChanges); const [projectVersions, setProjectVersions] = useState(data.projectVersions); const [isLeftPanelOpen, setIsLeftPanelOpen] = useState(true); const [isRightPanelOpen, setIsRightPanelOpen] = useState(true); const [isBottomPanelOpen, setIsBottomPanelOpen] = useState(true); const [leftPanelWidth, setLeftPanelWidth] = useState(null); const [checkStatus, setCheckStatus] = useState(null); const [closedOpenDocumentIds, setClosedOpenDocumentIds] = useState>(() => new Set()); const [pinnedOpenDocumentIds, setPinnedOpenDocumentIds] = useState>(() => new Set()); const leftPanelRef = useRef(null); const workspaceData = { ...data, authoringChanges, projectVersions }; const registerAuthoringChange = (change: AuthoringChange, version?: ProjectVersion) => { setAuthoringChanges((current) => [change, ...current.filter((item) => item.change_id !== change.change_id)]); if (version) { setProjectVersions((current) => [version, ...current.filter((item) => item.version_id !== version.version_id)]); } }; const workspaceModes: Array<{ id: WorkspaceMode; label: string; icon: React.ComponentType<{ className?: string }> }> = [ { id: "module", label: t.moduleMode, icon: Code2 }, { id: "form", label: t.formMode, icon: Layers3 }, { id: "properties", label: t.propertiesMode, icon: PanelRight }, { id: "events", label: t.eventsMode, icon: Braces }, { id: "flowchart", label: language === "ru" ? "Блок-схема" : "Flowchart", icon: GitCompareArrows }, { id: "versions", label: t.versionsMode, icon: History }, { id: "documentation", label: t.documentationMode, icon: FileText }, { id: "knowledge", label: t.knowledgeMode, icon: Sparkles }, { id: "learning", label: t.learningMode, icon: GraduationCap } ]; const allOpenDocuments = useMemo(() => buildOpenDocuments(data.metadataTree?.root, language), [data.metadataTree?.root, language]); const openDocuments = useMemo(() => { const visible = allOpenDocuments.filter((document) => pinnedOpenDocumentIds.has(document.id) || !closedOpenDocumentIds.has(document.id)); return visible.length > 0 ? visible : allOpenDocuments.slice(0, 1); }, [allOpenDocuments, closedOpenDocumentIds, pinnedOpenDocumentIds]); useEffect(() => { setAuthoringChanges(data.authoringChanges); setProjectVersions(data.projectVersions); setClosedOpenDocumentIds(new Set()); setPinnedOpenDocumentIds(new Set()); setCheckStatus(null); setActiveMode(normalizeWorkspaceMode(initialMode)); }, [data.projectId, data.authoringChanges, data.projectVersions, initialMode]); useEffect(() => { try { const saved = JSON.parse(window.localStorage.getItem(workspacePanelStorageKey(data.projectId)) ?? "{}") as { leftOpen?: boolean; rightOpen?: boolean; bottomOpen?: boolean; leftWidth?: number; }; setIsLeftPanelOpen(saved.leftOpen ?? true); setIsRightPanelOpen(saved.rightOpen ?? true); setIsBottomPanelOpen(saved.bottomOpen ?? true); setLeftPanelWidth(saved.leftWidth && saved.leftWidth >= 240 ? Math.min(saved.leftWidth, 520) : null); } catch { setIsLeftPanelOpen(true); setIsRightPanelOpen(true); setIsBottomPanelOpen(true); setLeftPanelWidth(null); } }, [data.projectId]); useEffect(() => { const payload = { leftOpen: isLeftPanelOpen, rightOpen: isRightPanelOpen, bottomOpen: isBottomPanelOpen, leftWidth: leftPanelWidth }; window.localStorage.setItem(workspacePanelStorageKey(data.projectId), JSON.stringify(payload)); }, [data.projectId, isBottomPanelOpen, isLeftPanelOpen, isRightPanelOpen, leftPanelWidth]); useEffect(() => { const panel = leftPanelRef.current; if (!panel || !isLeftPanelOpen) { return; } const observer = new ResizeObserver(([entry]) => { const width = Math.round(entry.contentRect.width); if (width >= 240 && width <= 520) { setLeftPanelWidth(width); } }); observer.observe(panel); return () => observer.disconnect(); }, [isLeftPanelOpen]); useEffect(() => { try { const saved = window.localStorage.getItem(openObjectsStorageKey(data.projectId)); if (!saved) { return; } const parsed = JSON.parse(saved) as { closed?: string[]; pinned?: string[] }; setClosedOpenDocumentIds(new Set(parsed.closed ?? [])); setPinnedOpenDocumentIds(new Set(parsed.pinned ?? [])); } catch { setClosedOpenDocumentIds(new Set()); setPinnedOpenDocumentIds(new Set()); } }, [data.projectId]); useEffect(() => { window.localStorage.setItem( openObjectsStorageKey(data.projectId), JSON.stringify({ closed: [...closedOpenDocumentIds], pinned: [...pinnedOpenDocumentIds] }) ); }, [closedOpenDocumentIds, data.projectId, pinnedOpenDocumentIds]); function closeOpenDocument(documentId: string) { const target = openDocuments.find((document) => document.id === documentId); if (!target || pinnedOpenDocumentIds.has(documentId) || openDocuments.length <= 1) { return; } const currentIndex = Math.max(0, openDocuments.findIndex((document) => document.id === documentId)); const nextDocument = openDocuments[currentIndex + 1] ?? openDocuments[currentIndex - 1] ?? openDocuments[0]; setClosedOpenDocumentIds((current) => new Set([...current, documentId])); if (target.mode === activeMode && nextDocument?.id !== target.id) { switchMode(nextDocument.mode, setActiveMode); } } function togglePinnedOpenDocument(documentId: string) { setPinnedOpenDocumentIds((current) => { const next = new Set(current); if (next.has(documentId)) { next.delete(documentId); } else { next.add(documentId); } return next; }); setClosedOpenDocumentIds((current) => { const next = new Set(current); next.delete(documentId); return next; }); } useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { const target = event.target as HTMLElement | null; const isEditableTarget = target?.tagName === "INPUT" || target?.tagName === "TEXTAREA" || target?.tagName === "SELECT" || target?.isContentEditable; if (event.altKey && !event.ctrlKey && !event.metaKey) { if (event.key === "1") { event.preventDefault(); setIsLeftPanelOpen((current) => !current); } else if (event.key === "2") { event.preventDefault(); setIsRightPanelOpen((current) => !current); } else if (event.key === "3") { event.preventDefault(); setIsBottomPanelOpen((current) => !current); } } if (!event.ctrlKey && !event.altKey && !event.metaKey && event.key === "F5") { event.preventDefault(); setIsBottomPanelOpen(true); setCheckStatus(language === "ru" ? "Проверка запущена" : "Check started"); return; } if (isEditableTarget || event.altKey || event.metaKey || !event.ctrlKey) { if (event.ctrlKey && !event.altKey && !event.metaKey && event.key.toLowerCase() === "k") { event.preventDefault(); document.querySelector("[data-global-search-input]")?.focus(); } return; } if (event.key.toLowerCase() === "k") { event.preventDefault(); document.querySelector("[data-global-search-input]")?.focus(); } else if (event.key === "Tab") { event.preventDefault(); switchOpenDocument(event.shiftKey ? -1 : 1, activeMode, openDocuments, setActiveMode); } else if (event.key.toLowerCase() === "w") { event.preventDefault(); const activeDocument = openDocuments.find((document) => document.mode === activeMode); if (activeDocument) { closeOpenDocument(activeDocument.id); } } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [activeMode, language, openDocuments, pinnedOpenDocumentIds]); return (
{isLeftPanelOpen ? (
) : ( setIsLeftPanelOpen(true)} /> )}
setIsBottomPanelOpen((current) => !current)} onToggleLeft={() => setIsLeftPanelOpen((current) => !current)} onToggleRight={() => setIsRightPanelOpen((current) => !current)} />
{isBottomPanelOpen ? : null}
{isRightPanelOpen ? (
) : ( setIsRightPanelOpen(true)} /> )}
{openDocuments.map((document) => { const isPinned = pinnedOpenDocumentIds.has(document.id); const isActive = activeMode === document.mode; return (
); })} {routineName ?? t.bslEditor}
); } function WorkspaceChrome({ checkStatus, isBottomPanelOpen, isLeftPanelOpen, isRightPanelOpen, language, onToggleBottom, onToggleLeft, onToggleRight }: Readonly<{ checkStatus: string | null; isBottomPanelOpen: boolean; isLeftPanelOpen: boolean; isRightPanelOpen: boolean; language: UiLanguage; onToggleBottom: () => void; onToggleLeft: () => void; onToggleRight: () => void; }>) { return (
{checkStatus ? (
{checkStatus}
) : (
Alt+1 Alt+2 Alt+3
)}
); } function IconToggle({ active, ariaLabel, icon: Icon, onClick }: Readonly<{ active: boolean; ariaLabel: string; icon: React.ComponentType<{ className?: string }>; onClick: () => void }>) { return ( ); } function PanelRail({ ariaLabel, icon: Icon, onClick }: Readonly<{ ariaLabel: string; icon: React.ComponentType<{ className?: string }>; onClick: () => void }>) { return ( ); } function switchOpenDocument( direction: -1 | 1, activeMode: WorkspaceMode, openDocuments: Array<{ mode: WorkspaceMode }>, setActiveMode: (mode: WorkspaceMode) => void ) { if (openDocuments.length === 0) { return; } const currentIndex = Math.max(0, openDocuments.findIndex((document) => document.mode === activeMode)); const nextIndex = (currentIndex + direction + openDocuments.length) % openDocuments.length; switchMode(openDocuments[nextIndex].mode, setActiveMode); } function openObjectsStorageKey(projectId: string) { return `sfera.open-objects.${projectId}`; } function workspacePanelStorageKey(projectId: string) { return `sfera.workspace-panels.${projectId}`; } function normalizeWorkspaceMode(value: string | undefined): WorkspaceMode { const allowedModes = new Set([ "overview", "module", "form", "properties", "events", "flowchart", "versions", "documentation", "knowledge", "learning" ]); return allowedModes.has(value as WorkspaceMode) ? (value as WorkspaceMode) : "module"; } function switchMode(nextMode: WorkspaceMode, setActiveMode: (mode: WorkspaceMode) => void) { setActiveMode(nextMode); const url = new URL(window.location.href); url.searchParams.set("mode", nextMode); window.history.replaceState(null, "", url); } function buildOpenDocuments(root: MetadataTreeNode | undefined, language: UiLanguage) { const t = messages[language]; const nodes = flattenMetadataTree(root, 6); const fallbackDocuments = [ { id: "overview", mode: "overview" as WorkspaceMode, label: t.overview, meta: t.projectWorkspace, icon: FileText }, { id: "flowchart", mode: "flowchart" as WorkspaceMode, label: language === "ru" ? "Блок-схема" : "Flowchart", meta: language === "ru" ? "Связи конфигурации" : "Configuration links", icon: GitCompareArrows }, { id: "module", mode: "module" as WorkspaceMode, label: t.moduleMode, meta: t.bslEditor, icon: Code2 }, { id: "form", mode: "form" as WorkspaceMode, label: t.formMode, meta: t.formDesigner, icon: Layers3 }, { id: "task", mode: "knowledge" as WorkspaceMode, label: "TASK-123", meta: t.activeTask, icon: Bot } ]; if (nodes.length === 0) { return fallbackDocuments; } const documents = nodes.map((node) => ({ id: node.id, mode: modeForMetadataNode(node), label: node.label, meta: node.kind, icon: iconForMetadataNode(node) })); const existingModes = new Set(documents.map((document) => document.mode)); return [ ...documents, ...fallbackDocuments.filter((document) => !existingModes.has(document.mode)) ].slice(0, 6); } function flattenMetadataTree(root: MetadataTreeNode | undefined, limit: number) { const result: MetadataTreeNode[] = []; const visit = (node: MetadataTreeNode) => { if (result.length >= limit) { return; } if (node.qualified_name && node.kind !== "CONFIGURATOR" && node.kind !== "COMMON") { result.push(node); } for (const child of node.children) { visit(child); if (result.length >= limit) { return; } } }; if (root) { visit(root); } return result; } function findMetadataTreeNode(root: MetadataTreeNode | undefined, routineName?: string) { if (!root || !routineName) { return undefined; } const normalizedRoutine = routineName.toLocaleLowerCase("ru-RU"); const stack = [root]; while (stack.length > 0) { const node = stack.pop()!; const qualifiedName = node.qualified_name?.toLocaleLowerCase("ru-RU"); if (node.label.toLocaleLowerCase("ru-RU") === normalizedRoutine || qualifiedName === normalizedRoutine) { return node; } stack.push(...node.children); } return undefined; } function childLabelsByName(node: MetadataTreeNode | undefined, groupName: string) { const group = node?.children.find((child) => child.label === groupName); return group?.children.map((child) => child.label) ?? []; } function childGroupCountByName(node: MetadataTreeNode | undefined, groupName: string) { return node?.children.find((child) => child.label === groupName)?.count ?? 0; } function modeForMetadataNode(node: MetadataTreeNode): WorkspaceMode { const value = `${node.kind} ${node.label} ${node.icon}`.toLocaleLowerCase("ru-RU"); if (value.includes("form") || value.includes("форма")) { return "form"; } if (value.includes("event") || value.includes("событ")) { return "events"; } if (value.includes("flowchart") || value.includes("блок-схем")) { return "flowchart"; } return "module"; } function iconForMetadataNode(node: MetadataTreeNode): React.ComponentType<{ className?: string }> { const value = `${node.kind} ${node.label} ${node.icon}`.toLocaleLowerCase("ru-RU"); if (value.includes("form") || value.includes("форма")) { return Layers3; } if (value.includes("register") || value.includes("регистр")) { return DatabaseIcon; } if (value.includes("event") || value.includes("событ")) { return Braces; } if (value.includes("flowchart") || value.includes("блок-схем")) { return GitCompareArrows; } if (value.includes("knowledge") || value.includes("знан")) { return BrainIcon; } return FileCode2; } function ObjectTree({ data, language, title, nodes, selectedObject }: Readonly<{ data: ProjectWorkspaceData; language: UiLanguage; title: string; nodes: SirNode[]; selectedObject?: SirNode; }>) { const t = messages[language]; const projectId = data.projectId; const [query, setQuery] = useState(""); const [treeFilter, setTreeFilter] = useState<"all" | "metadata" | "sfera" | "environments">("all"); const normalizedQuery = normalizeTreeSearch(query); const commonSectionIcons = Object.fromEntries(commonMetadataSections.map((item) => [item.label, item.iconKind])); const commonSections = (data.metadataCatalog?.common_branch_children ?? commonMetadataSections.map((item) => item.label)).map((label) => ({ label, iconKind: commonSectionIcons[label] ?? kindForTreeLabel(label) })); const metadataTypes = data.metadataCatalog?.types.filter((spec) => !["COMMON", "COMMON_MODULE", "EXTENSION"].includes(spec.code)) ?? fallbackMetadataTypeSpecs; const filteredNodes = filterSirNodesForTree(nodes, normalizedQuery); const filteredCommonSections = commonSections.filter((item) => matchesTreeSearch(item.label, normalizedQuery)); const filteredMetadataTypes = metadataTypes.filter((spec) => { const specNodes = filteredNodes.filter((node) => nodeMatchesMetadataSpec(node, spec)); return matchesTreeSearch(`${spec.tree_branch} ${spec.russian_name} ${spec.code}`, normalizedQuery) || specNodes.length > 0; }); const showMetadataTree = treeFilter === "all" || treeFilter === "metadata"; const showSferaTree = treeFilter === "all" || treeFilter === "sfera"; const showEnvironmentTree = treeFilter === "all" || treeFilter === "environments"; const sferaSections = [t.aiHandlers, t.semanticRules, t.reviewPolicies, t.knowledgeBindings, t.agentCommands, t.rollbackTemplates].filter((item) => matchesTreeSearch(item, normalizedQuery)); const sferaWorkspaces = ["Задачи", "Проверки", "Версии и изменения", "Инциденты", "Runtime", "Знания", "Документация", "Паттерны", "Релизы", "Агенты"].filter((item) => matchesTreeSearch(item, normalizedQuery)); const environments = ["Dev", "Test", "Stage", "Prod"].filter((item) => matchesTreeSearch(item, normalizedQuery)); const useServerTree = Boolean(data.metadataTree && (nodes.length === 0 || (data.snapshot?.node_count ?? 0) > 20000)); if (useServerTree && data.metadataTree) { return ( ); } return ( ); } function normalizeTreeSearch(value: string) { return value.trim().toLocaleLowerCase("ru-RU"); } function matchesTreeSearch(value: string, query: string) { return query.length === 0 || value.toLocaleLowerCase("ru-RU").includes(query); } function filterSirNodesForTree(nodes: SirNode[], query: string) { if (!query) { return nodes; } return nodes.filter((node) => matchesTreeSearch(`${node.name} ${node.kind} ${node.qualified_name}`, query)); } function ConfigurationLikeRoot({ commonSections, language, metadataTypes, nodes, projectId, selectedObject, title }: Readonly<{ commonSections: Array<{ label: string; iconKind: OneCIconKind }>; language: UiLanguage; metadataTypes: MetadataTypeSpec[]; nodes: SirNode[]; projectId: string; selectedObject?: SirNode; title: string; }>) { return ( {commonSections.map((item) => ( ))} {metadataTypes.map((spec) => ( ))} ); } function ServerMetadataTreeNode({ language, node, projectId }: Readonly<{ language: UiLanguage; node: MetadataTreeNode; projectId: string; }>) { const iconKind = isOneCIconKind(node.icon) ? node.icon : kindForTreeLabel(node.label); const title = node.count > 0 ? `${node.label} (${node.count})` : node.label; if (node.children.length === 0) { return ( ); } return ( {node.children.map((child) => ( ))} ); } function CommonMetadataSection({ iconKind, language, nodes, projectId, title }: Readonly<{ iconKind: OneCIconKind; language: UiLanguage; nodes: SirNode[]; projectId: string; title: string; }>) { const sectionNodes = nodes.filter((node) => nodeMatchesCommonSection(node, title)); if (sectionNodes.length === 0) { return ; } return ( {sectionNodes.map((node) => ( ))} ); } function MetadataTypeBranch({ language, nodes, projectId, selectedObject, spec }: Readonly<{ language: UiLanguage; nodes: SirNode[]; projectId: string; selectedObject?: SirNode; spec: MetadataTypeSpec; }>) { const objectNodes = nodes.filter((node) => nodeMatchesMetadataSpec(node, spec)); const iconKind = iconKindForMetadataSpec(spec); const propertySummary = spec.properties?.slice(0, 5).join(", "); const actionSummary = spec.context_actions?.slice(0, 4).join(", "); return ( 0} iconKind={iconKind} storageScope={projectId} title={spec.tree_branch}> {propertySummary ? (
Свойства: {propertySummary}
) : null} {actionSummary ? (
Действия: {actionSummary}
) : null} {objectNodes.map((node) => ( {[...spec.child_groups, ...spec.module_kinds, "Версии", "Проверки", "Инциденты", "Знания"].map((group) => ( ))} ))}
); } function editorHref(language: UiLanguage, projectId: string, mode: WorkspaceMode, routine: string) { return `/editor?lang=${language}&project=${encodeURIComponent(projectId)}&mode=${mode}&routine=${encodeURIComponent(routine)}`; } const fallbackMetadataTypeSpecs: MetadataTypeSpec[] = [ { code: "CONSTANT", russian_name: "Константа", tree_branch: "Константы", icon: "constant", child_groups: ["Формы", "Команды", "Права"], module_kinds: [] }, { code: "CATALOG", russian_name: "Справочник", tree_branch: "Справочники", icon: "catalog", child_groups: ["Реквизиты", "Табличные части", "Формы", "Команды", "Макеты", "Права", "Предопределенные данные"], module_kinds: ["Модуль объекта", "Модуль менеджера"] }, { code: "DOCUMENT", russian_name: "Документ", tree_branch: "Документы", icon: "document", child_groups: ["Реквизиты", "Табличные части", "Формы", "Команды", "Макеты", "Права", "Движения", "Последовательности", "Нумераторы"], module_kinds: ["Модуль объекта", "Модуль менеджера"] }, { code: "DOCUMENT_JOURNAL", russian_name: "Журнал документов", tree_branch: "Журналы документов", icon: "journal", child_groups: ["Графы", "Формы", "Команды", "Макеты", "Права"], module_kinds: ["Модуль менеджера"] }, { code: "ENUM", russian_name: "Перечисление", tree_branch: "Перечисления", icon: "enum", child_groups: ["Значения", "Формы", "Команды", "Макеты", "Права"], module_kinds: ["Модуль менеджера"] }, { code: "REPORT", russian_name: "Отчет", tree_branch: "Отчеты", icon: "report", child_groups: ["Реквизиты", "Табличные части", "Формы", "Команды", "Макеты", "Табличные документы", "СКД", "Варианты отчета", "Настройки", "Хранилище вариантов", "Хранилище настроек", "Справка", "Права"], module_kinds: ["Модуль объекта", "Модуль менеджера"] }, { code: "DATA_PROCESSOR", russian_name: "Обработка", tree_branch: "Обработки", icon: "processing", child_groups: ["Реквизиты", "Табличные части", "Формы", "Команды", "Макеты", "Права"], module_kinds: ["Модуль объекта", "Модуль менеджера"] }, { code: "CHART_OF_CHARACTERISTIC_TYPES", russian_name: "План видов характеристик", tree_branch: "Планы видов характеристик", icon: "plan", child_groups: ["Реквизиты", "Табличные части", "Формы", "Команды", "Макеты", "Права", "Предопределенные данные"], module_kinds: ["Модуль объекта", "Модуль менеджера"] }, { code: "CHART_OF_ACCOUNTS", russian_name: "План счетов", tree_branch: "Планы счетов", icon: "plan", child_groups: ["Признаки учета", "Признаки учета субконто", "Табличные части", "Формы", "Команды", "Макеты", "Права", "Предопределенные данные"], module_kinds: ["Модуль объекта", "Модуль менеджера"] }, { code: "CHART_OF_CALCULATION_TYPES", russian_name: "План видов расчета", tree_branch: "Планы видов расчета", icon: "plan", child_groups: ["Реквизиты", "Табличные части", "Вытесняющие виды расчета", "Ведущие виды расчета", "Базовые виды расчета", "Формы", "Команды", "Макеты", "Права"], module_kinds: ["Модуль объекта", "Модуль менеджера"] }, { code: "INFORMATION_REGISTER", russian_name: "Регистр сведений", tree_branch: "Регистры сведений", icon: "register", child_groups: ["Измерения", "Ресурсы", "Реквизиты", "Формы", "Команды", "Макеты", "Кто пишет", "Кто читает", "Права"], module_kinds: ["Модуль набора записей", "Модуль менеджера"] }, { code: "ACCUMULATION_REGISTER", russian_name: "Регистр накопления", tree_branch: "Регистры накопления", icon: "register", child_groups: ["Измерения", "Ресурсы", "Реквизиты", "Формы", "Команды", "Макеты", "Кто пишет", "Кто читает", "Права"], module_kinds: ["Модуль набора записей", "Модуль менеджера"] }, { code: "ACCOUNTING_REGISTER", russian_name: "Регистр бухгалтерии", tree_branch: "Регистры бухгалтерии", icon: "register", child_groups: ["Измерения", "Ресурсы", "Реквизиты", "Признаки учета", "Признаки учета субконто", "Формы", "Команды", "Макеты", "Права"], module_kinds: ["Модуль набора записей", "Модуль менеджера"] }, { code: "CALCULATION_REGISTER", russian_name: "Регистр расчета", tree_branch: "Регистры расчета", icon: "register", child_groups: ["Измерения", "Ресурсы", "Реквизиты", "Перерасчеты", "Формы", "Команды", "Макеты", "Права"], module_kinds: ["Модуль набора записей", "Модуль менеджера"] }, { code: "BUSINESS_PROCESS", russian_name: "Бизнес-процесс", tree_branch: "Бизнес-процессы", icon: "business-process", child_groups: ["Реквизиты", "Табличные части", "Карта маршрута", "Точки маршрута", "Формы", "Команды", "Макеты", "Права"], module_kinds: ["Модуль объекта", "Модуль менеджера"] }, { code: "TASK", russian_name: "Задача 1С", tree_branch: "Задачи", icon: "task", child_groups: ["Реквизиты", "Табличные части", "Адресация", "Формы", "Команды", "Макеты", "Права"], module_kinds: ["Модуль объекта", "Модуль менеджера"] }, { code: "EXCHANGE_PLAN", russian_name: "План обмена", tree_branch: "Планы обмена", icon: "exchange-plan", child_groups: ["Реквизиты", "Команды", "Права"], module_kinds: ["Модуль объекта", "Модуль менеджера"] }, { code: "EVENT_SUBSCRIPTION", russian_name: "Подписка на событие", tree_branch: "Подписки на события", icon: "event", child_groups: ["События"], module_kinds: ["Модуль объекта", "Модуль менеджера"] }, { code: "SCHEDULED_JOB", russian_name: "Регламентное задание", tree_branch: "Регламентные задания", icon: "scheduled-job", child_groups: ["Команды", "Права"], module_kinds: ["Модуль объекта", "Модуль менеджера"] }, { code: "EXTERNAL_DATA_SOURCE", russian_name: "Внешний источник данных", tree_branch: "Внешние источники данных", icon: "external-source", child_groups: ["Таблицы", "Кубы", "Функции", "Формы", "Команды", "Макеты"], module_kinds: [] }, { code: "WEB_SERVICE", russian_name: "Web-сервис", tree_branch: "Web-сервисы", icon: "service", child_groups: ["Операции", "Параметры", "Модуль"], module_kinds: [] }, { code: "HTTP_SERVICE", russian_name: "HTTP-сервис", tree_branch: "HTTP-сервисы", icon: "service", child_groups: ["Шаблоны URL", "Методы", "Модуль"], module_kinds: [] }, { code: "WS_REFERENCE", russian_name: "WS-ссылка", tree_branch: "WS-ссылки", icon: "service", child_groups: ["Операции", "Параметры"], module_kinds: [] }, { code: "WEBSOCKET_CLIENT", russian_name: "WebSocket-клиент", tree_branch: "WebSocket-клиенты", icon: "service", child_groups: ["Модуль"], module_kinds: [] }, { code: "INTEGRATION_SERVICE", russian_name: "Сервис интеграции", tree_branch: "Сервисы интеграции", icon: "service", child_groups: ["Каналы", "Сообщения", "Модуль"], module_kinds: [] }, { code: "BOT", russian_name: "Бот", tree_branch: "Боты", icon: "service", child_groups: ["Команды", "Модуль"], module_kinds: [] } ]; const commonMetadataSections: Array<{ label: string; iconKind: OneCIconKind }> = [ { label: "Подсистемы", iconKind: "subsystem" }, { label: "Общие модули", iconKind: "module" }, { label: "Параметры сеанса", iconKind: "settings" }, { label: "Роли", iconKind: "role" }, { label: "Общие реквизиты", iconKind: "attribute" }, { label: "Планы обмена", iconKind: "exchange-plan" }, { label: "Критерии отбора", iconKind: "settings" }, { label: "Подписки на события", iconKind: "event" }, { label: "Регламентные задания", iconKind: "scheduled-job" }, { label: "Боты", iconKind: "service" }, { label: "Функциональные опции", iconKind: "settings" }, { label: "Параметры функциональных опций", iconKind: "settings" }, { label: "Определяемые типы", iconKind: "plan" }, { label: "Хранилища настроек", iconKind: "settings" }, { label: "Общие команды", iconKind: "command" }, { label: "Группы команд", iconKind: "command" }, { label: "Общие формы", iconKind: "form" }, { label: "Общие макеты", iconKind: "layout" }, { label: "Общие картинки", iconKind: "layout" }, { label: "XDTO-пакеты", iconKind: "integration" }, { label: "Web-сервисы", iconKind: "web" }, { label: "HTTP-сервисы", iconKind: "web" }, { label: "WS-ссылки", iconKind: "web" }, { label: "WebSocket-клиенты", iconKind: "web" }, { label: "Сервисы интеграции", iconKind: "integration" }, { label: "Цвета палитры", iconKind: "palette" }, { label: "Элементы стиля", iconKind: "palette" }, { label: "Стили", iconKind: "palette" }, { label: "Языки", iconKind: "language" } ]; function iconKindForMetadataSpec(spec: MetadataTypeSpec): OneCIconKind { return isOneCIconKind(spec.icon) ? spec.icon : kindForTreeLabel(spec.tree_branch); } function isOneCIconKind(value: string): value is OneCIconKind { return value in oneCIconFiles; } function modeForTreeGroup(group: string): WorkspaceMode { const value = group.toLowerCase(); if (value.includes("форм")) return "form"; if (value.includes("событ")) return "events"; if (value.includes("верс")) return "versions"; if (value.includes("знан")) return "knowledge"; return "module"; } function nodeMatchesCommonSection(node: SirNode, sectionTitle: string): boolean { const kind = node.kind.toUpperCase(); const qualifiedName = normalizeQualifiedName(node.qualified_name); if (sectionTitle === "Общие модули") return kind === "COMMON_MODULE" || qualifiedName.startsWith("общиймодуль."); if (sectionTitle === "Роли") return kind === "ROLE" || qualifiedName.startsWith("роль."); if (sectionTitle === "Планы обмена") return kind === "EXCHANGE_PLAN" || qualifiedName.startsWith("планобмена."); if (sectionTitle === "Подписки на события") return kind === "EVENT_SUBSCRIPTION" || qualifiedName.startsWith("подписканасобытие."); if (sectionTitle === "Регламентные задания") return kind === "SCHEDULED_JOB" || qualifiedName.startsWith("регламентноезадание."); if (sectionTitle === "Общие формы") return kind === "FORM" && qualifiedName.startsWith("общаяформа."); if (sectionTitle === "Общие команды") return kind === "COMMAND" && qualifiedName.startsWith("общаякоманда."); if (sectionTitle === "HTTP-сервисы") return qualifiedName.startsWith("httpсервис.") || qualifiedName.startsWith("http-сервис."); if (sectionTitle === "Web-сервисы") return qualifiedName.startsWith("webсервис.") || qualifiedName.startsWith("web-сервис."); if (sectionTitle === "WS-ссылки") return qualifiedName.startsWith("wsссылка.") || qualifiedName.startsWith("ws-ссылка."); return false; } function nodeMatchesMetadataSpec(node: SirNode, spec: MetadataTypeSpec): boolean { const kind = node.kind.toUpperCase(); const qualifiedName = normalizeQualifiedName(node.qualified_name); const topLevel = isTopLevelMetadataQualifiedName(qualifiedName); switch (spec.code) { case "CONSTANT": return topLevel && qualifiedName.startsWith("константа."); case "CATALOG": return kind === "CATALOG" || (topLevel && qualifiedName.startsWith("справочник.")); case "DOCUMENT": return kind === "DOCUMENT" || (topLevel && qualifiedName.startsWith("документ.")); case "DOCUMENT_JOURNAL": return topLevel && qualifiedName.startsWith("журналдокументов."); case "ENUM": return topLevel && qualifiedName.startsWith("перечисление."); case "REPORT": return topLevel && (qualifiedName.startsWith("отчет.") || qualifiedName.startsWith("отчёт.")); case "DATA_PROCESSOR": return topLevel && qualifiedName.startsWith("обработка."); case "CHART_OF_CHARACTERISTIC_TYPES": return topLevel && qualifiedName.startsWith("планвидовхарактеристик."); case "CHART_OF_ACCOUNTS": return topLevel && qualifiedName.startsWith("плансчетов."); case "CHART_OF_CALCULATION_TYPES": return topLevel && (qualifiedName.startsWith("планвидоврасчета.") || qualifiedName.startsWith("планвидоврасчёта.")); case "INFORMATION_REGISTER": return topLevel && qualifiedName.startsWith("регистрсведений."); case "ACCUMULATION_REGISTER": return topLevel && qualifiedName.startsWith("регистрнакопления."); case "ACCOUNTING_REGISTER": return topLevel && qualifiedName.startsWith("регистрбухгалтерии."); case "CALCULATION_REGISTER": return topLevel && (qualifiedName.startsWith("регистррасчета.") || qualifiedName.startsWith("регистррасчёта.")); case "BUSINESS_PROCESS": return kind === "BUSINESS_PROCESS" || (topLevel && qualifiedName.startsWith("бизнеспроцесс.")); case "TASK": return kind === "TASK" || (topLevel && qualifiedName.startsWith("задача.")); case "EXTERNAL_DATA_SOURCE": return topLevel && qualifiedName.startsWith("внешнийисточникданных."); case "EXCHANGE_PLAN": return kind === "EXCHANGE_PLAN" || topLevel && qualifiedName.startsWith("планобмена."); case "EVENT_SUBSCRIPTION": return kind === "EVENT_SUBSCRIPTION" || topLevel && qualifiedName.startsWith("подписканасобытие."); case "SCHEDULED_JOB": return kind === "SCHEDULED_JOB" || topLevel && qualifiedName.startsWith("регламентноезадание."); default: return false; } } function normalizeQualifiedName(value: string) { return value.toLowerCase().replace(/\s+/g, ""); } function isTopLevelMetadataQualifiedName(value: string) { return (value.match(/\./g)?.length ?? 0) === 1; } function kindForTreeLabel(label: string): OneCIconKind { const value = label.toLowerCase(); if (value.includes("справочник")) return "catalog"; if (value.includes("документ") && !value.includes("журнал")) return "document"; if (value.includes("журнал")) return "journal"; if (value.includes("перечислен")) return "enum"; if (value.includes("отчет") || value.includes("отчёт")) return "report"; if (value.includes("обработ")) return "processing"; if (value.includes("событ")) return "event"; if (value.includes("обмен") || value.includes("обмена")) return "exchange-plan"; if (value.includes("регистр")) return "register"; if (value.includes("план")) return "plan"; if (value.includes("бизнес")) return "business-process"; if (value.includes("задач")) return "task"; if (value.includes("источник")) return "external-source"; if (value.includes("реквизит") || value.includes("измерен") || value.includes("ресурс")) return "attribute"; if (value.includes("таблич")) return "tabular"; if (value.includes("форм")) return "form"; if (value.includes("команд")) return "command"; if (value.includes("макет")) return "layout"; if (value.includes("модул")) return "module"; if (value.includes("прав") || value.includes("рол")) return "role"; if (value.includes("http") || value.includes("web") || value.includes("ws-")) return "web"; if (value.includes("интеграц")) return "integration"; if (value.includes("регламент")) return "scheduled-job"; if (value.includes("констант")) return "constant"; if (value.includes("язык")) return "language"; if (value.includes("стил") || value.includes("палитр")) return "palette"; if (value.includes("настрой") || value.includes("параметр") || value.includes("индекс")) return "settings"; return "common"; } function kindForSirNode(kind: string): OneCIconKind { const value = kind.toUpperCase(); if (value.includes("EXCHANGE")) return "exchange-plan"; if (value.includes("DOCUMENT")) return "document"; if (value.includes("CATALOG")) return "catalog"; if (value.includes("REGISTER")) return "register"; if (value.includes("REPORT")) return "report"; if (value.includes("PROCESSING")) return "processing"; if (value.includes("FORM")) return "form"; if (value.includes("COMMAND")) return "command"; if (value.includes("ATTRIBUTE")) return "attribute"; if (value.includes("TABULAR")) return "tabular"; if (value.includes("EVENT")) return "event"; if (value.includes("SCHEDULED_JOB")) return "scheduled-job"; if (value.includes("MODULE") || value.includes("PROCEDURE") || value.includes("FUNCTION")) return "module"; return "common"; } function EditorTreeGroup({ title, children }: Readonly<{ title: string; children: React.ReactNode }>) { return (
{title}
{children}
); } function EditorTreeBranch({ children, defaultOpen = false, icon: Icon = ListTree, iconKind, storageScope, title }: Readonly<{ children: React.ReactNode; defaultOpen?: boolean; icon?: React.ComponentType<{ className?: string }>; iconKind?: OneCIconKind; storageScope?: string; title: string; }>) { const storageKey = `sfera.editor-tree.branch.${storageScope ? `${storageScope}.` : ""}${title}`; const [open, setOpen] = useState(() => { if (typeof window === "undefined") { return defaultOpen; } const saved = window.localStorage.getItem(storageKey); return saved ? saved === "true" : defaultOpen; }); useEffect(() => { const saved = window.localStorage.getItem(storageKey); setOpen(saved ? saved === "true" : defaultOpen); }, [defaultOpen, storageKey]); useEffect(() => { window.localStorage.setItem(storageKey, String(open)); }, [open, storageKey]); return (
setOpen(event.currentTarget.open)} open={open}>
{children}
); } function OneCTreeIcon({ kind }: Readonly<{ kind: OneCIconKind }>) { const icon = oneCIconFiles[kind] ?? oneCIconFiles.common; return ( ); } function EditorTreeLeaf({ href = "#", iconKind = "attribute", label }: Readonly<{ href?: string; iconKind?: OneCIconKind; label: string }>) { return ( {label} ); } function EditorTreeLink({ active, href, iconKind, label, meta }: Readonly<{ active: boolean; href: string; iconKind: OneCIconKind; label: string; meta: string; }>) { return ( {label} {meta} ); } function WorkspaceModePanel({ activeMode, data, language, onAuthoringChangeApplied, selectedObject, selectedMetadataNode, setActiveMode }: Readonly<{ activeMode: WorkspaceMode; data: ProjectWorkspaceData; language: UiLanguage; onAuthoringChangeApplied: (change: AuthoringChange, version?: ProjectVersion) => void; selectedObject?: SirNode; selectedMetadataNode?: MetadataTreeNode; setActiveMode: (mode: WorkspaceMode) => void; }>) { return (
); } function WorkspaceModeBody({ activeMode, data, language, onAuthoringChangeApplied, selectedObject, selectedMetadataNode, setActiveMode }: Readonly<{ activeMode: WorkspaceMode; data: ProjectWorkspaceData; language: UiLanguage; onAuthoringChangeApplied: (change: AuthoringChange, version?: ProjectVersion) => void; selectedObject?: SirNode; selectedMetadataNode?: MetadataTreeNode; setActiveMode: (mode: WorkspaceMode) => void; }>) { if (activeMode === "overview") { return ; } if (activeMode === "form") { return ; } if (activeMode === "properties" || activeMode === "events") { return ( ); } if (activeMode === "versions") { return ; } if (activeMode === "flowchart") { return ; } if (activeMode === "documentation" || activeMode === "knowledge" || activeMode === "learning") { return ; } return ( ); } function ObjectInternalTabs({ activeMode, language, setActiveMode }: Readonly<{ activeMode: WorkspaceMode; language: UiLanguage; setActiveMode: (mode: WorkspaceMode) => void; }>) { const t = messages[language]; const tabs: Array<{ label: string; mode: WorkspaceMode }> = [ { label: t.objectOverview, mode: "overview" }, { label: t.objectAttributes, mode: "properties" }, { label: t.forms, mode: "form" }, { label: t.moduleMode, mode: "module" }, { label: language === "ru" ? "Блок-схема" : "Flowchart", mode: "flowchart" }, { label: t.permissionState, mode: "properties" }, { label: t.versionsMode, mode: "versions" }, { label: t.authoringHistory, mode: "versions" } ]; return (
{tabs.map((tab) => ( ))}
); } function ObjectOverviewPanel({ data, language, selectedMetadataNode, selectedObject }: Readonly<{ data: ProjectWorkspaceData; language: UiLanguage; selectedMetadataNode?: MetadataTreeNode; selectedObject?: SirNode; }>) { const t = messages[language]; const context = data.authoringPreview?.context; const selectedName = context?.object?.qualified_name ?? selectedObject?.qualified_name ?? selectedMetadataNode?.qualified_name ?? selectedMetadataNode?.label ?? t.none; const selectedForms = data.selectedObjectUi?.forms ?? []; const formRows = selectedForms.length > 0 ? selectedForms.map((item) => item.form.name) : childLabelsByName(selectedMetadataNode, "Формы"); const formCount = childGroupCountByName(selectedMetadataNode, "Формы"); const visibleFormRows = formRows.length > 0 ? formRows : formCount > 0 ? [`${formCount} ${language === "ru" ? "форм, загружаются при раскрытии" : "forms, load on expand"}`] : []; const schema = data.selectedObjectSchema; const impact = data.selectedObjectImpact; const commandRows = impact?.commands.map((node) => node.name) ?? context?.commands.map((node) => node.name) ?? childLabelsByName(selectedMetadataNode, "Команды"); const formElementCount = selectedForms.reduce((total, item) => total + item.elements.length, 0); return (
{t.selectedObject}

{selectedName}

{t.objectOverviewDescription}

node.name)} /> `${section.tabular_section.name}${section.columns.length ? ` (${section.columns.length})` : ""}`)} /> node.qualified_name) ?? context?.available_methods.slice(0, 5) ?? []} /> node.qualified_name) ?? context?.writes.map((node) => node.qualified_name) ?? []} />
); } function OverviewMetric({ label, value }: Readonly<{ label: string; value: string }>) { return (
{label}
{value}
); } function FlowchartPanel({ data, language, selectedMetadataNode, selectedObject }: Readonly<{ data: ProjectWorkspaceData; language: UiLanguage; selectedMetadataNode?: MetadataTreeNode; selectedObject?: SirNode; }>) { const [chart, setChart] = useState(data.flowchart); const [selectedKind, setSelectedKind] = useState(null); const [loadingFocus, setLoadingFocus] = useState(null); const [error, setError] = useState(null); const selectedQualifiedName = selectedMetadataNode?.qualified_name ?? selectedObject?.qualified_name ?? null; const nodesById = useMemo(() => new Map((chart?.nodes ?? []).map((node) => [node.id, node])), [chart?.nodes]); const kindObjects = useMemo(() => { if (!selectedKind) { return []; } return (data.exportSnapshot?.nodes ?? []) .filter((node) => node.kind === selectedKind) .sort((left, right) => left.qualified_name.localeCompare(right.qualified_name, "ru")) .slice(0, 80); }, [data.exportSnapshot?.nodes, selectedKind]); useEffect(() => { setChart(data.flowchart); setSelectedKind(null); setError(null); }, [data.flowchart, data.projectId]); async function loadFocus(focus: string | null) { setError(null); setLoadingFocus(focus ?? "overview"); try { const next = await getProjectFlowchart(data.projectId, { apiUrl: data.apiUrl, focus, depth: focus ? 2 : undefined, limit: focus ? 120 : 80 }); setChart(next); if (!focus) { setSelectedKind(null); } } catch (loadError) { setError(loadError instanceof Error ? loadError.message : "Не удалось загрузить блок-схему"); } finally { setLoadingFocus(null); } } function handleNodeClick(node: FlowchartNode) { if (node.id.startsWith("kind:")) { setSelectedKind(node.kind); return; } void loadFocus(node.id); } const selectedKindLabel = selectedKind ? flowchartKindLabel(selectedKind, language) : null; const activeNode = chart?.focus ? nodesById.get(chart.focus) : null; const positions = chart ? flowchartPositions(chart.nodes) : new Map(); return (
{loadingFocus ? (
{language === "ru" ? "Загрузка схемы..." : "Loading chart..."}
) : null} {error ? (
{error}
) : null}
{(chart?.edges ?? []).map((edge) => { const source = positions.get(edge.source); const target = positions.get(edge.target); if (!source || !target) { return null; } const midX = (source.x + target.x) / 2; const midY = (source.y + target.y) / 2; return ( 1 ? 2 : 1.4} x1={source.x} x2={target.x} y1={source.y} y2={target.y} /> {edge.count > 1 ? `${edge.label} ${edge.count}` : edge.label} ); })} {(chart?.nodes ?? []).map((node) => { const position = positions.get(node.id) ?? { x: 80, y: 80 }; return ( ); })}
); } function FlowchartActionButton({ active = false, disabled, label, onClick }: Readonly<{ active?: boolean; disabled?: boolean; label: string; onClick: () => void }>) { return ( ); } function flowchartPositions(nodes: ProjectFlowchart["nodes"]) { const positions = new Map(); if (nodes.length === 0) { return positions; } const overview = nodes.some((node) => node.id.startsWith("kind:")); const levels = new Map(); nodes.forEach((node, index) => { const level = overview ? Math.floor(index / 4) : node.level; const current = levels.get(level) ?? []; current.push(node); levels.set(level, current); }); const sortedLevels = [...levels.entries()].sort(([left], [right]) => left - right); sortedLevels.forEach(([level, levelNodes], levelIndex) => { const columns = overview ? Math.min(4, Math.max(1, levelNodes.length)) : 1; levelNodes.forEach((node, index) => { positions.set(node.id, overview ? { x: 150 + (index % columns) * 230, y: 90 + level * 150 } : { x: 180 + levelIndex * 260, y: 90 + index * Math.max(84, Math.min(150, 520 / Math.max(1, levelNodes.length - 1 || 1))) }); }); }); return positions; } function flowchartKindLabel(kind: string, language: UiLanguage) { const ru: Record = { CATALOG: "Справочник", DOCUMENT: "Документ", COMMON_MODULE: "Общий модуль", MODULE: "Модуль", PROCEDURE: "Процедура", FUNCTION: "Функция", FORM: "Форма", COMMAND: "Команда", ROLE: "Роль", ATTRIBUTE: "Реквизит", TABULAR_SECTION: "Табличная часть", QUERY: "Запрос", TABLE: "Таблица", REGISTER: "Регистр", REPORT: "Отчет", DATA_PROCESSOR: "Обработка", HTTP_SERVICE: "HTTP-сервис", INTEGRATION_ENDPOINT: "Интеграция", SCHEDULED_JOB: "Регламентное задание", EXTENSION: "Расширение" }; return language === "ru" ? ru[kind] ?? kind : kind.replaceAll("_", " ").toLowerCase(); } function iconKindForFlowchart(kind: string): OneCIconKind { if (kind.includes("DOCUMENT")) return "document"; if (kind.includes("CATALOG")) return "catalog"; if (kind.includes("MODULE") || kind === "PROCEDURE" || kind === "FUNCTION" || kind === "QUERY") return "module"; if (kind.includes("FORM")) return "form"; if (kind.includes("COMMAND")) return "command"; if (kind.includes("ROLE")) return "role"; if (kind.includes("REPORT")) return "report"; if (kind.includes("REGISTER")) return "register"; if (kind.includes("HTTP") || kind.includes("INTEGRATION")) return "web"; if (kind.includes("TABULAR")) return "tabular"; if (kind.includes("ATTRIBUTE")) return "attribute"; return "tree"; } function modeForFlowchartNode(node: { kind: string }): WorkspaceMode { if (node.kind === "FORM") return "form"; if (node.kind === "ROLE" || node.kind === "ATTRIBUTE" || node.kind === "TABULAR_SECTION") return "properties"; return "module"; } function OverviewList({ rows, title }: Readonly<{ rows: string[]; title: string }>) { return (
{title}
{rows.length === 0 ? (
нет
) : ( rows.map((row) =>
{row}
) )}
); } function EditorPanel({ data, language, onAuthoringChangeApplied, selectedMetadataNode, selectedObject, setActiveMode }: Readonly<{ data: ProjectWorkspaceData; language: UiLanguage; onAuthoringChangeApplied: (change: AuthoringChange, version?: ProjectVersion) => void; selectedMetadataNode?: MetadataTreeNode; selectedObject?: SirNode; setActiveMode: (mode: WorkspaceMode) => void; }>) { const t = messages[language]; const initialProposedText = data.editorSourceText ?? data.editorProposedText ?? sampleCode.join("\n"); const [proposedText, setProposedText] = useState(initialProposedText); const [semanticDiffPreview, setSemanticDiffPreview] = useState(data.semanticDiffPreview); const [isRefreshingPreview, setIsRefreshingPreview] = useState(false); const sourceText = data.editorSourceText && data.editorSourceText.trim() ? data.editorSourceText : sampleCode.join("\n"); const monacoValue = proposedText || sourceText; const isRealSourceLoaded = Boolean(data.editorSourceText && !data.editorSourceText.startsWith("// Код модуля не загружен.")); const hasEditorChanges = normalizeEditorText(proposedText) !== normalizeEditorText(sourceText); const applyEnabled = semanticDiffPreview?.version_preview?.apply_available ?? false; const activeGuardChecks = semanticDiffPreview?.checks ?? data.authoringPreview?.checks ?? []; const blockedGuardChecks = getBlockingGuardChecks(activeGuardChecks); const firstBlockedGuardCheck = blockedGuardChecks[0] ?? null; const isGuardBlocked = blockedGuardChecks.length > 0; const canApplyPayload = Boolean(data.editorSelectedObject && data.editorSourceText && proposedText && hasEditorChanges); const navigationTarget = data.authoringPreview?.context.routine ?? data.authoringPreview?.context.object ?? selectedObject ?? null; const initialSymbolQuery = data.authoringPreview?.context.routine?.name ?? selectedMetadataNode?.label ?? selectedObject?.name ?? "Проведение"; const [applyState, setApplyState] = useState<"idle" | "pending" | "applied" | "error">("idle"); const [applyMessage, setApplyMessage] = useState(""); const [symbolQuery, setSymbolQuery] = useState(initialSymbolQuery); const [symbolResults, setSymbolResults] = useState([]); const [symbolReferences, setSymbolReferences] = useState(null); const [symbolDefinition, setSymbolDefinition] = useState(null); const [symbolState, setSymbolState] = useState<"idle" | "loading" | "error">("idle"); const [symbolMessage, setSymbolMessage] = useState(""); const isApplying = applyState === "pending"; const applyLabel = applyState === "pending" ? t.applying : applyState === "applied" ? t.savedToSfera : t.applyToSfera; const activeSymbol = symbolResults[0] ?? (navigationTarget ? { node: navigationTarget, source: sourceLocationFromNode(selectedObject) } : null); const routineName = data.authoringPreview?.context.routine?.name ?? data.editorSelectedRoutine ?? "Procedure"; const guardClauseSnippet = `Процедура ${routineName}\n Если ЗначениеЗаполнено(${routineName}) Тогда\n Возврат;\n КонецЕсли;\nКонецПроцедуры`; const extractProcedureSnippet = `&НаКлиенте\nПроцедура ExtractedProcedure()\n // TODO: перенести логику в новую процедуру\nКонецПроцедуры`; const knowledgeLinkSnippet = `// TODO [Knowledge]: ссылку на профильный шаблон/регламент добавить здесь`; useEffect(() => { setProposedText(data.editorSourceText ?? data.editorProposedText ?? sampleCode.join("\n")); }, [data.editorProposedText, data.editorSourceText, data.editorSelectedObject, data.editorSelectedRoutine]); useEffect(() => { setSemanticDiffPreview(data.semanticDiffPreview); }, [data.semanticDiffPreview]); useEffect(() => { if (!hasEditorChanges || !isRealSourceLoaded) { return; } const timeout = window.setTimeout(() => { void refreshSemanticDiff(proposedText); }, 700); return () => window.clearTimeout(timeout); }, [hasEditorChanges, isRealSourceLoaded, proposedText]); async function refreshSemanticDiff(nextProposedText: string) { const objectName = data.editorSelectedObject ?? data.authoringPreview?.context.object?.name ?? selectedMetadataNode?.label ?? null; if (!objectName || !data.editorSourceText || !nextProposedText) { setSemanticDiffPreview(null); return; } setIsRefreshingPreview(true); try { const response = await getAuthoringSemanticDiffPreview( data.projectId, { object_name: objectName, routine_name: data.editorSelectedRoutine ?? data.authoringPreview?.context.routine?.name ?? null, original_text: data.editorSourceText, proposed_text: nextProposedText, ...(await ensureAuthoringSession(data.projectId, data.apiUrl)) }, data.apiUrl ); setSemanticDiffPreview(response); } catch { setSemanticDiffPreview(null); } finally { setIsRefreshingPreview(false); } } function setSuggestedText(next: string) { setProposedText((current) => { const nextProposedText = `${current.trimEnd()}\n\n${next}`; void refreshSemanticDiff(nextProposedText); return nextProposedText; }); } function applyInsertGuardClause() { setSuggestedText(guardClauseSnippet); } function applyExtractProcedure() { setSuggestedText(extractProcedureSnippet); } function applyAddKnowledgeLink() { setSuggestedText(knowledgeLinkSnippet); } function openSemanticDiff() { setActiveMode("versions"); } function openKnowledgePanel() { setActiveMode("knowledge"); } function openDocumentationPanel() { setActiveMode("documentation"); } async function handleSymbolSearch() { const query = symbolQuery.trim(); if (!query) { setSymbolResults([]); setSymbolReferences(null); setSymbolDefinition(null); setSymbolMessage(language === "ru" ? "Введите имя символа." : "Enter a symbol name."); return; } setSymbolState("loading"); setSymbolMessage(""); try { const results = await searchProjectSymbols(data.projectId, query, { apiUrl: data.apiUrl, limit: 12 }); setSymbolResults(results); setSymbolReferences(null); setSymbolDefinition(null); setSymbolState("idle"); setSymbolMessage(results.length === 0 ? t.emptyList : `${t.search}: ${results.length}`); } catch (error) { setSymbolState("error"); setSymbolMessage(error instanceof Error ? error.message : language === "ru" ? "Не удалось выполнить поиск символов." : "Symbol search failed."); } } async function handleDefinition(lineageId = activeSymbol?.node.lineage_id) { if (!lineageId) { setSymbolMessage(language === "ru" ? "Нет выбранного символа." : "No selected symbol."); return; } setSymbolState("loading"); setSymbolMessage(""); try { const definition = await getProjectSymbolDefinition(data.projectId, lineageId, data.apiUrl); setSymbolDefinition(definition); setSymbolState("idle"); setSymbolMessage(`${language === "ru" ? "Определение" : "Definition"}: ${formatSourceLocation(definition.source, language)}`); } catch (error) { setSymbolState("error"); setSymbolMessage(error instanceof Error ? error.message : language === "ru" ? "Не удалось открыть определение." : "Definition lookup failed."); } } async function handleReferences(lineageId = activeSymbol?.node.lineage_id) { if (!lineageId) { setSymbolMessage(language === "ru" ? "Нет выбранного символа." : "No selected symbol."); return; } setSymbolState("loading"); setSymbolMessage(""); try { const references = await getProjectSymbolReferences(data.projectId, lineageId, { apiUrl: data.apiUrl, direction: "incoming" }); setSymbolReferences(references); setSymbolState("idle"); setSymbolMessage(`${t.referencesPanel}: ${references.references.length}`); } catch (error) { setSymbolState("error"); setSymbolMessage(error instanceof Error ? error.message : language === "ru" ? "Не удалось найти использования." : "References lookup failed."); } } async function handleApplyToSfera() { const versionPreview = semanticDiffPreview?.version_preview; if (isGuardBlocked) { setApplyState("error"); setApplyMessage(firstBlockedGuardCheck?.message ?? t.blocked); return; } if (!versionPreview?.apply_available || !canApplyPayload) { setApplyState("error"); setApplyMessage(t.previewRequired); return; } setApplyState("pending"); setApplyMessage(""); try { const apiUrl = data.apiUrl ?? (typeof window === "undefined" ? resolveApiUrl() : resolveApiUrl(window.location.host)); const authoringSession = await ensureAuthoringSession(data.projectId, apiUrl); const response = await applyAuthoringChangeSet( data.projectId, { object_name: data.editorSelectedObject ?? data.authoringPreview?.context.object?.name ?? selectedMetadataNode?.label ?? null, routine_name: data.editorSelectedRoutine ?? data.authoringPreview?.context.routine?.name ?? null, original_text: data.editorSourceText ?? sourceText, proposed_text: proposedText ?? sourceText, task_id: authoringSession.task_id, session_id: authoringSession.session_id, user_id: authoringSession.user_id, estimated_tokens: 2550, expected_next_version_id: versionPreview.next_version_id, approved_by: authoringSession.user_id, approval_note: t.guardedApplyNote, apply_to_production: false }, apiUrl ); onAuthoringChangeApplied( { change_id: response.change_id, project_id: response.project_id, status: response.status, target: response.preview.target, version_id: response.version.version_id, approved_by: authoringSession.user_id, approval_note: t.guardedApplyNote, task_id: response.version.task_id, session_id: response.version.session_id, added_lines: response.preview.added_lines, removed_lines: response.preview.removed_lines, production_applied: response.production_applied }, versionToSummary(response.version) ); setApplyState("applied"); setApplyMessage(`${formatChangeStatus(response.status, language)}: ${response.version.version_id}`); } catch (error) { setApplyState("error"); setApplyMessage(error instanceof Error ? error.message : language === "ru" ? "Ошибка применения." : "Apply failed."); } } return (
handleDefinition()} /> handleReferences()} /> {isGuardBlocked ? ( {language === "ru" ? `Блокировки: ${blockedGuardChecks.length}` : `Blocked: ${blockedGuardChecks.length}`} ) : ( {canApplyPayload ? t.workspaceApplyReady : t.previewRequired} )}
); } function SymbolNavigationPanel({ definition, language, message, onDefinition, onQueryChange, onReferences, onSearch, query, references, results, state }: Readonly<{ definition: SymbolResult | null; language: UiLanguage; message: string; onDefinition: (lineageId?: string) => void; onQueryChange: (value: string) => void; onReferences: (lineageId?: string) => void; onSearch: () => void; query: string; references: SymbolReferences | null; results: SymbolResult[]; state: "idle" | "loading" | "error"; }>) { const t = messages[language]; const referenceRows = references?.references.slice(0, 8) ?? []; return (
onQueryChange(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); onSearch(); } }} placeholder={language === "ru" ? "Найти символ" : "Find symbol"} value={query} /> 0 ? "success" : "neutral"}>{referenceRows.length}
{message ? (
{message}
) : null}
{results.length === 0 ? (
{language === "ru" ? "Выполните поиск символа или откройте использования выбранного объекта." : "Search a symbol or open references for the selected object."}
) : ( results.map((result) => (
{result.node.qualified_name}
{result.node.kind} · {formatSourceLocation(result.source, language)}
)) )}
{definition ? (
{definition.node.qualified_name}
{formatSourceLocation(definition.source, language)}
) : null} {referenceRows.length > 0 ? (
{referenceRows.map((reference) => (
{reference.source?.qualified_name ?? reference.target?.qualified_name ?? reference.kind}
{reference.kind} · {reference.direction} · {formatSourceLocation(reference.location, language)}
))}
) : null}
); } function EditorToolbarButton({ actionId, disabled = false, icon: Icon, label, onClick }: Readonly<{ actionId?: string; disabled?: boolean; icon: React.ComponentType<{ className?: string }>; label: string; onClick?: () => void; }>) { return ( ); } function FastBslEditor({ data, onChange, readOnly, value }: Readonly<{ data: ProjectWorkspaceData; onChange: (value: string) => void; readOnly: boolean; value: string; }>) { const textareaRef = useRef(null); const highlightRef = useRef(null); const [completionState, setCompletionState] = useState<{ items: BslCompletionItem[]; left: number; prefix: string; receiver: string | null; replaceStart: number; replaceEnd: number; selectedIndex: number; top: number; } | null>(null); const localCompletionItems = useMemo(() => buildLocalBslCompletionItems(data), [data]); const highlightedLines = useMemo(() => value.split("\n"), [value]); function syncScroll() { const textarea = textareaRef.current; const highlight = highlightRef.current; if (!textarea || !highlight) { return; } highlight.scrollTop = textarea.scrollTop; highlight.scrollLeft = textarea.scrollLeft; } async function refreshCompletions(nextValue = value, caret = textareaRef.current?.selectionStart ?? 0, force = false) { const context = getBslCompletionContext(nextValue, caret); if (!context && !force) { setCompletionState(null); return; } const normalizedContext = context ?? { prefix: "", receiver: null, replaceStart: caret, replaceEnd: caret }; let items = filterCompletionItems(localCompletionItems, normalizedContext.prefix, normalizedContext.receiver); if (normalizedContext.receiver) { try { const remoteItems = await getBslCompletions(data.projectId, { apiUrl: data.apiUrl, receiver: normalizedContext.receiver, q: normalizedContext.prefix, qualifiedName: data.editorSelectedObject, limit: 80 }); items = mergeCompletionItems(remoteItems, items); } catch { items = filterCompletionItems(localCompletionItems, normalizedContext.prefix, normalizedContext.receiver); } } setCompletionState( items.length ? { ...normalizedContext, items, ...completionPopupPosition(nextValue, caret, textareaRef.current), selectedIndex: 0 } : null ); } function updateValue(nextValue: string) { onChange(nextValue); window.requestAnimationFrame(() => { const caret = textareaRef.current?.selectionStart ?? nextValue.length; void refreshCompletions(nextValue, caret); }); } function insertCompletion(item: BslCompletionItem) { const textarea = textareaRef.current; if (!textarea || !completionState) { return; } const insertText = item.insert_text ?? item.label; const nextValue = `${value.slice(0, completionState.replaceStart)}${insertText}${value.slice(completionState.replaceEnd)}`; const nextCaret = completionState.replaceStart + insertText.length; onChange(nextValue); setCompletionState(null); window.requestAnimationFrame(() => { textarea.focus(); textarea.selectionStart = nextCaret; textarea.selectionEnd = nextCaret; syncScroll(); }); } function handleKeyDown(event: React.KeyboardEvent) { if (event.ctrlKey && event.code === "Space") { event.preventDefault(); void refreshCompletions(value, event.currentTarget.selectionStart, true); return; } if (!completionState) { return; } if (event.key === "ArrowDown") { event.preventDefault(); setCompletionState((current) => current ? { ...current, selectedIndex: (current.selectedIndex + 1) % current.items.length } : current); } else if (event.key === "ArrowUp") { event.preventDefault(); setCompletionState((current) => current ? { ...current, selectedIndex: (current.selectedIndex - 1 + current.items.length) % current.items.length } : current); } else if (event.key === "Enter" || event.key === "Tab") { event.preventDefault(); insertCompletion(completionState.items[completionState.selectedIndex]); } else if (event.key === "Escape") { event.preventDefault(); setCompletionState(null); } } return (