From 7dc0088bd2b895a40d8e11b3c08669ded0ccb483 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Wed, 9 Jul 2025 17:09:45 +0300 Subject: [PATCH] F: Implement TermGraph view --- rsconcept/frontend/src/app/global-dialogs.tsx | 5 + .../dlg-show-term-graph.tsx | 13 +- .../dlg-show-term-graph/tg-readonly-flow.tsx | 111 ++++++++++++++++++ .../toolbar-graph-filter.tsx | 54 +++++++++ ...ar-constituents.tsx => toolbar-schema.tsx} | 12 +- .../side-panel/view-schema.tsx | 4 +- .../components/term-graph/graph/tg-node.tsx | 8 +- .../term-graph/toolbar-focused-cst.tsx | 34 ++---- .../rsform-page/editor-term-graph/tg-flow.tsx | 11 +- .../editor-term-graph/toolbar-term-graph.tsx | 21 +--- .../src/features/rsform/stores/term-graph.ts | 4 + rsconcept/frontend/src/stores/dialogs.ts | 42 +++---- 12 files changed, 235 insertions(+), 84 deletions(-) create mode 100644 rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/tg-readonly-flow.tsx create mode 100644 rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/toolbar-graph-filter.tsx rename rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/{toolbar-constituents.tsx => toolbar-schema.tsx} (97%) diff --git a/rsconcept/frontend/src/app/global-dialogs.tsx b/rsconcept/frontend/src/app/global-dialogs.tsx index ab203bb2..6caf8d87 100644 --- a/rsconcept/frontend/src/app/global-dialogs.tsx +++ b/rsconcept/frontend/src/app/global-dialogs.tsx @@ -131,6 +131,9 @@ const DlgOssSettings = React.lazy(() => const DlgEditCst = React.lazy(() => import('@/features/rsform/dialogs/dlg-edit-cst').then(module => ({ default: module.DlgEditCst })) ); +const DlgShowTermGraph = React.lazy(() => + import('@/features/oss/dialogs/dlg-show-term-graph').then(module => ({ default: module.DlgShowTermGraph })) +); export const GlobalDialogs = () => { const active = useDialogsStore(state => state.active); @@ -193,5 +196,7 @@ export const GlobalDialogs = () => { return ; case DialogType.EDIT_CONSTITUENTA: return ; + case DialogType.SHOW_TERM_GRAPH: + return ; } }; diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/dlg-show-term-graph.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/dlg-show-term-graph.tsx index 4a1e4076..eac9fbcd 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/dlg-show-term-graph.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/dlg-show-term-graph.tsx @@ -4,7 +4,6 @@ import { ReactFlowProvider } from 'reactflow'; import { urls, useConceptNavigation } from '@/app'; // import { useDialogsStore } from '@/stores/dialogs'; import { type IRSForm } from '@/features/rsform'; -import { TGFlow } from '@/features/rsform/pages/rsform-page/editor-term-graph/tg-flow'; import { RSTabID } from '@/features/rsform/pages/rsform-page/rsedit-context'; import { MiniButton } from '@/components/control'; @@ -12,6 +11,8 @@ import { IconRSForm } from '@/components/icons'; import { ModalView } from '@/components/modal'; import { useDialogsStore } from '@/stores/dialogs'; +import { TGReadonlyFlow } from './tg-readonly-flow'; + export interface DlgShowTermGraphProps { schema: IRSForm; } @@ -32,20 +33,16 @@ export function DlgShowTermGraph() { } return ( - + } onClick={navigateToSchema} /> - {/* TGFlow expects schema from context, so you may need to refactor TGFlow to accept schema as prop if needed */} - + ); diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/tg-readonly-flow.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/tg-readonly-flow.tsx new file mode 100644 index 00000000..80c64604 --- /dev/null +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/tg-readonly-flow.tsx @@ -0,0 +1,111 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { type Edge, MarkerType, type Node, useEdgesState, useNodesState } from 'reactflow'; + +import { TGEdgeTypes } from '@/features/rsform/components/term-graph/graph/tg-edge-types'; +import { TGNodeTypes } from '@/features/rsform/components/term-graph/graph/tg-node-types'; +import { SelectColoring } from '@/features/rsform/components/term-graph/select-coloring'; +import { ToolbarFocusedCst } from '@/features/rsform/components/term-graph/toolbar-focused-cst'; +import { applyLayout, produceFilteredGraph, type TGNodeData } from '@/features/rsform/models/graph-api'; +import { type IConstituenta, type IRSForm } from '@/features/rsform/models/rsform'; +import { flowOptions } from '@/features/rsform/pages/rsform-page/editor-term-graph/tg-flow'; +import { useTermGraphStore } from '@/features/rsform/stores/term-graph'; + +import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow'; +import { PARAMETER } from '@/utils/constants'; + +import ToolbarGraphFilter from './toolbar-graph-filter'; + +export interface TGReadonlyFlowProps { + schema: IRSForm; +} + +export function TGReadonlyFlow({ schema }: TGReadonlyFlowProps) { + const [focusCst, setFocusCst] = useState(null); + + const filter = useTermGraphStore(state => state.filter); + const filteredGraph = produceFilteredGraph(schema, filter, focusCst); + + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges] = useEdgesState([]); + const { fitView, viewportInitialized } = useReactFlow(); + + useEffect(() => { + if (!viewportInitialized) { + return; + } + const newNodes: Node[] = []; + filteredGraph.nodes.forEach(node => { + const cst = schema.cstByID.get(node.id); + if (cst) { + newNodes.push({ + id: String(node.id), + type: 'concept', + position: { x: 0, y: 0 }, + data: { cst: cst, focused: focusCst?.id === cst.id } + }); + } + }); + + const newEdges: Edge[] = []; + let edgeID = 1; + filteredGraph.nodes.forEach(source => { + source.outputs.forEach(target => { + if (newNodes.find(node => node.id === String(target))) { + newEdges.push({ + id: String(edgeID), + source: String(source.id), + target: String(target), + type: 'termEdge', + focusable: false, + markerEnd: { + type: MarkerType.ArrowClosed, + width: 20, + height: 20 + } + }); + edgeID += 1; + } + }); + }); + + applyLayout(newNodes, newEdges, !filter.noText); + + setNodes(newNodes); + setEdges(newEdges); + + setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.minimalTimeout); + }, [schema, filteredGraph, setNodes, setEdges, filter.noText, fitView, viewportInitialized, focusCst]); + + function handleNodeContextMenu(event: React.MouseEvent, node: TGNodeData) { + event.preventDefault(); + event.stopPropagation(); + setFocusCst(focusCst?.id === node.data.cst.id ? null : node.data.cst); + } + + return ( +
+
+ + setFocusCst(null)} /> +
+
+ +
+ + event.preventDefault()} + onNodeContextMenu={handleNodeContextMenu} + /> +
+ ); +} diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/toolbar-graph-filter.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/toolbar-graph-filter.tsx new file mode 100644 index 00000000..d6b150cf --- /dev/null +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/toolbar-graph-filter.tsx @@ -0,0 +1,54 @@ +import { useReactFlow } from 'reactflow'; + +import { useTermGraphStore } from '@/features/rsform/stores/term-graph'; + +import { MiniButton } from '@/components/control'; +import { IconClustering, IconClusteringOff, IconFitImage, IconText, IconTextOff } from '@/components/icons'; +import { PARAMETER } from '@/utils/constants'; + +import { flowOptions } from '../../pages/oss-page/editor-oss-graph/oss-flow'; + +export default function ToolbarGraphFilter() { + const filter = useTermGraphStore(state => state.filter); + const toggleText = useTermGraphStore(state => state.toggleText); + const toggleClustering = useTermGraphStore(state => state.toggleClustering); + const { fitView } = useReactFlow(); + + function handleFitView() { + setTimeout(() => { + fitView(flowOptions.fitViewOptions); + }, PARAMETER.minimalTimeout); + } + + return ( +
+ } + onClick={handleFitView} + /> + + ) : ( + + ) + } + onClick={toggleText} + /> + + ) : ( + + ) + } + onClick={toggleClustering} + /> +
+ ); +} diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/toolbar-constituents.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/toolbar-schema.tsx similarity index 97% rename from rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/toolbar-constituents.tsx rename to rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/toolbar-schema.tsx index 8e392f07..2a9952da 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/toolbar-constituents.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/toolbar-schema.tsx @@ -24,7 +24,7 @@ import { useDialogsStore } from '@/stores/dialogs'; import { PARAMETER, prefixes } from '@/utils/constants'; import { type RO } from '@/utils/meta'; -interface ToolbarConstituentsProps { +interface ToolbarSchemaProps { schema: IRSForm; isMutable: boolean; activeCst: IConstituenta | null; @@ -34,7 +34,7 @@ interface ToolbarConstituentsProps { className?: string; } -export function ToolbarConstituents({ +export function ToolbarSchema({ schema, activeCst, setActive, @@ -42,7 +42,7 @@ export function ToolbarConstituents({ onEditActive, isMutable, className -}: ToolbarConstituentsProps) { +}: ToolbarSchemaProps) { const router = useConceptNavigation(); const isProcessing = useMutatingRSForm(); const searchText = useCstSearchStore(state => state.query); @@ -175,6 +175,10 @@ export function ToolbarConstituents({ showTypeGraph({ items: typeInfo }); } + function handleShowTermGraph() { + showTermGraph({ schema: schema }); + } + return (
} title='Граф термов' - onClick={() => showTermGraph({ schema })} + onClick={handleShowTermGraph} /> } diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/view-schema.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/view-schema.tsx index 7664f2da..ad3dc49a 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/view-schema.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/side-panel/view-schema.tsx @@ -8,7 +8,7 @@ import { ViewConstituents } from '@/features/rsform/components/view-constituents import { useFitHeight } from '@/stores/app-layout'; import { useDialogsStore } from '@/stores/dialogs'; -import { ToolbarConstituents } from './toolbar-constituents'; +import { ToolbarSchema } from './toolbar-schema'; interface ViewSchemaProps { schemaID: number; @@ -29,7 +29,7 @@ export function ViewSchema({ schemaID, isMutable }: ViewSchemaProps) { return (
- Типизация: ${labelCstTypification(cst)}`; + return `${cst.alias}: ${cst.term_resolved}
Типизация: ${labelCstTypification( + cst + )}
Содержание: ${ + isBasicConcept(cst.cst_type) ? cst.convention : cst.definition_resolved || cst.definition_formal || cst.convention + }`; } diff --git a/rsconcept/frontend/src/features/rsform/components/term-graph/toolbar-focused-cst.tsx b/rsconcept/frontend/src/features/rsform/components/term-graph/toolbar-focused-cst.tsx index 2bfd7464..94d94ed0 100644 --- a/rsconcept/frontend/src/features/rsform/components/term-graph/toolbar-focused-cst.tsx +++ b/rsconcept/frontend/src/features/rsform/components/term-graph/toolbar-focused-cst.tsx @@ -4,27 +4,19 @@ import { MiniButton } from '@/components/control'; import { IconGraphInputs, IconGraphOutputs, IconReset } from '@/components/icons'; import { cn } from '@/components/utils'; +import { useTermGraphStore } from '../../stores/term-graph'; + interface ToolbarFocusedCstProps { className?: string; focus: IConstituenta | null; resetFocus: () => void; - - showInputs: boolean; - toggleShowInputs: () => void; - - showOutputs: boolean; - toggleShowOutputs: () => void; } -export function ToolbarFocusedCst({ - focus, - resetFocus, - className, - showInputs, - toggleShowInputs, - showOutputs, - toggleShowOutputs -}: ToolbarFocusedCstProps) { +export function ToolbarFocusedCst({ focus, resetFocus, className }: ToolbarFocusedCstProps) { + const filter = useTermGraphStore(state => state.filter); + const toggleFocusInputs = useTermGraphStore(state => state.toggleFocusInputs); + const toggleFocusOutputs = useTermGraphStore(state => state.toggleFocusOutputs); + if (!focus) { return null; } @@ -44,14 +36,14 @@ export function ToolbarFocusedCst({ onClick={resetFocus} /> } - onClick={toggleShowInputs} + title={filter.focusShowInputs ? 'Скрыть поставщиков' : 'Отобразить поставщиков'} + icon={} + onClick={toggleFocusInputs} /> } - onClick={toggleShowOutputs} + title={filter.focusShowOutputs ? 'Скрыть потребителей' : 'Отобразить потребителей'} + icon={} + onClick={toggleFocusOutputs} />
); diff --git a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/tg-flow.tsx b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/tg-flow.tsx index a6218347..7e3b01c0 100644 --- a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/tg-flow.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/tg-flow.tsx @@ -55,8 +55,6 @@ export function TGFlow() { const [edges, setEdges] = useEdgesState([]); const filter = useTermGraphStore(state => state.filter); - const toggleFocusInputs = useTermGraphStore(state => state.toggleFocusInputs); - const toggleFocusOutputs = useTermGraphStore(state => state.toggleFocusOutputs); const { filteredGraph, hidden } = useFilteredGraph(); function onSelectionChange({ nodes }: { nodes: Node[] }) { @@ -170,14 +168,7 @@ export function TGFlow() {
- setFocus(null)} - showInputs={filter.focusShowInputs} - toggleShowInputs={toggleFocusInputs} - showOutputs={filter.focusShowOutputs} - toggleShowOutputs={toggleFocusOutputs} - /> + setFocus(null)} /> {!focusCst ? ( state.showShowTypeGraph); const showParams = useDialogsStore(state => state.showGraphParams); const filter = useTermGraphStore(state => state.filter); - const setFilter = useTermGraphStore(state => state.setFilter); + const toggleText = useTermGraphStore(state => state.toggleText); + const toggleClustering = useTermGraphStore(state => state.toggleClustering); const { fitView } = useReactFlow(); @@ -68,13 +69,6 @@ export function ToolbarTermGraph() { promptDeleteCst(); } - function handleToggleNoText() { - setFilter({ - ...filter, - noText: !filter.noText - }); - } - function handleFitView() { setTimeout(() => { fitView(flowOptions.fitViewOptions); @@ -88,13 +82,6 @@ export function ToolbarTermGraph() { } } - function handleFoldDerived() { - setFilter({ - ...filter, - foldDerived: !filter.foldDerived - }); - } - function handleSelectOss(event: React.MouseEvent, newValue: ILibraryItemReference) { navigateOss(newValue.id, event.ctrlKey || event.metaKey); } @@ -127,7 +114,7 @@ export function ToolbarTermGraph() { ) } - onClick={handleToggleNoText} + onClick={toggleText} /> ) } - onClick={handleFoldDerived} + onClick={toggleClustering} /> {isContentEditable ? ( void; toggleFocusInputs: () => void; toggleFocusOutputs: () => void; + toggleText: () => void; + toggleClustering: () => void; foldHidden: boolean; toggleFoldHidden: () => void; @@ -69,6 +71,8 @@ export const useTermGraphStore = create()( set(state => ({ filter: { ...state.filter, focusShowInputs: !state.filter.focusShowInputs } })), toggleFocusOutputs: () => set(state => ({ filter: { ...state.filter, focusShowOutputs: !state.filter.focusShowOutputs } })), + toggleText: () => set(state => ({ filter: { ...state.filter, noText: !state.filter.noText } })), + toggleClustering: () => set(state => ({ filter: { ...state.filter, foldDerived: !state.filter.foldDerived } })), foldHidden: false, toggleFoldHidden: () => set(state => ({ foldHidden: !state.foldHidden })), diff --git a/rsconcept/frontend/src/stores/dialogs.ts b/rsconcept/frontend/src/stores/dialogs.ts index a279258d..b20b3670 100644 --- a/rsconcept/frontend/src/stores/dialogs.ts +++ b/rsconcept/frontend/src/stores/dialogs.ts @@ -39,31 +39,31 @@ export const DialogType = { RENAME_CONSTITUENTA: 6, CREATE_BLOCK: 7, - EDIT_BLOCK: 25, + EDIT_BLOCK: 8, - CREATE_OPERATION: 8, - EDIT_OPERATION: 9, - DELETE_OPERATION: 10, - CHANGE_INPUT_SCHEMA: 11, - RELOCATE_CONSTITUENTS: 12, - OSS_SETTINGS: 26, - EDIT_CONSTITUENTA: 27, + CREATE_OPERATION: 9, + EDIT_OPERATION: 10, + DELETE_OPERATION: 11, + CHANGE_INPUT_SCHEMA: 12, + RELOCATE_CONSTITUENTS: 13, + OSS_SETTINGS: 14, + EDIT_CONSTITUENTA: 15, - CLONE_LIBRARY_ITEM: 13, - UPLOAD_RSFORM: 14, - EDIT_EDITORS: 15, - EDIT_VERSIONS: 16, - CHANGE_LOCATION: 17, + CLONE_LIBRARY_ITEM: 16, + UPLOAD_RSFORM: 17, + EDIT_EDITORS: 18, + EDIT_VERSIONS: 19, + CHANGE_LOCATION: 20, - EDIT_REFERENCE: 18, - EDIT_WORD_FORMS: 19, - INLINE_SYNTHESIS: 20, + EDIT_REFERENCE: 21, + EDIT_WORD_FORMS: 22, + INLINE_SYNTHESIS: 23, - SHOW_QR_CODE: 21, - SHOW_AST: 22, - SHOW_TYPE_GRAPH: 23, - GRAPH_PARAMETERS: 24, - SHOW_TERM_GRAPH: 25 + SHOW_QR_CODE: 24, + SHOW_AST: 25, + SHOW_TYPE_GRAPH: 26, + GRAPH_PARAMETERS: 27, + SHOW_TERM_GRAPH: 28 } as const; export type DialogType = (typeof DialogType)[keyof typeof DialogType];