F: Implement TermGraph view
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run
Frontend CI / notify-failure (push) Blocked by required conditions

This commit is contained in:
Ivan 2025-07-09 17:10:42 +03:00
parent bc7195d617
commit aeebbc6d5b
12 changed files with 235 additions and 84 deletions

View File

@ -131,6 +131,9 @@ const DlgOssSettings = React.lazy(() =>
const DlgEditCst = React.lazy(() => const DlgEditCst = React.lazy(() =>
import('@/features/rsform/dialogs/dlg-edit-cst').then(module => ({ default: module.DlgEditCst })) 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 = () => { export const GlobalDialogs = () => {
const active = useDialogsStore(state => state.active); const active = useDialogsStore(state => state.active);
@ -193,5 +196,7 @@ export const GlobalDialogs = () => {
return <DlgUploadRSForm />; return <DlgUploadRSForm />;
case DialogType.EDIT_CONSTITUENTA: case DialogType.EDIT_CONSTITUENTA:
return <DlgEditCst />; return <DlgEditCst />;
case DialogType.SHOW_TERM_GRAPH:
return <DlgShowTermGraph />;
} }
}; };

View File

@ -4,7 +4,6 @@ import { ReactFlowProvider } from 'reactflow';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
// import { useDialogsStore } from '@/stores/dialogs'; // import { useDialogsStore } from '@/stores/dialogs';
import { type IRSForm } from '@/features/rsform'; 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 { RSTabID } from '@/features/rsform/pages/rsform-page/rsedit-context';
import { MiniButton } from '@/components/control'; import { MiniButton } from '@/components/control';
@ -12,6 +11,8 @@ import { IconRSForm } from '@/components/icons';
import { ModalView } from '@/components/modal'; import { ModalView } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { TGReadonlyFlow } from './tg-readonly-flow';
export interface DlgShowTermGraphProps { export interface DlgShowTermGraphProps {
schema: IRSForm; schema: IRSForm;
} }
@ -32,20 +33,16 @@ export function DlgShowTermGraph() {
} }
return ( return (
<ModalView <ModalView className='relative w-[calc(100dvw-3rem)] h-[calc(100dvh-3rem)]' fullScreen header='Граф термов'>
className='relative w-[calc(100dvw-3rem)] h-[calc(100dvh-3rem)] cc-mask-sides'
fullScreen
header='Граф термов'
>
<MiniButton <MiniButton
title='Открыть концептуальную схему' title='Открыть концептуальную схему'
noPadding noPadding
className='absolute z-pop top-2 left-2'
icon={<IconRSForm size='1.25rem' className='text-primary' />} icon={<IconRSForm size='1.25rem' className='text-primary' />}
onClick={navigateToSchema} onClick={navigateToSchema}
/> />
<ReactFlowProvider> <ReactFlowProvider>
{/* TGFlow expects schema from context, so you may need to refactor TGFlow to accept schema as prop if needed */} <TGReadonlyFlow schema={schema} />
<TGFlow />
</ReactFlowProvider> </ReactFlowProvider>
</ModalView> </ModalView>
); );

View File

@ -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<IConstituenta | null>(null);
const filter = useTermGraphStore(state => state.filter);
const filteredGraph = produceFilteredGraph(schema, filter, focusCst);
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges] = useEdgesState<Edge>([]);
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<Element>, node: TGNodeData) {
event.preventDefault();
event.stopPropagation();
setFocusCst(focusCst?.id === node.data.cst.id ? null : node.data.cst);
}
return (
<div className='relative w-full h-full flex flex-col'>
<div className='cc-tab-tools flex flex-col mt-2 items-center rounded-b-2xl backdrop-blur-xs'>
<ToolbarGraphFilter />
<ToolbarFocusedCst className='-translate-x-9' focus={focusCst} resetFocus={() => setFocusCst(null)} />
</div>
<div className='absolute z-pop top-24 sm:top-16 left-2 sm:left-3 w-54 flex flex-col pointer-events-none'>
<SelectColoring schema={schema} />
</div>
<DiagramFlow
{...flowOptions}
height='100%'
className='cc-mask-sides w-full h-full'
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
nodeTypes={TGNodeTypes}
edgeTypes={TGEdgeTypes}
onContextMenu={event => event.preventDefault()}
onNodeContextMenu={handleNodeContextMenu}
/>
</div>
);
}

View File

@ -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 (
<div className='flex flex-row gap-2'>
<MiniButton
title='Граф целиком'
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
onClick={handleFitView}
/>
<MiniButton
title={!filter.noText ? 'Скрыть текст' : 'Отобразить текст'}
icon={
!filter.noText ? (
<IconText size='1.25rem' className='icon-green' />
) : (
<IconTextOff size='1.25rem' className='icon-primary' />
)
}
onClick={toggleText}
/>
<MiniButton
title={!filter.foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные'}
icon={
!filter.foldDerived ? (
<IconClustering size='1.25rem' className='icon-green' />
) : (
<IconClusteringOff size='1.25rem' className='icon-primary' />
)
}
onClick={toggleClustering}
/>
</div>
);
}

View File

@ -24,7 +24,7 @@ import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER, prefixes } from '@/utils/constants'; import { PARAMETER, prefixes } from '@/utils/constants';
import { type RO } from '@/utils/meta'; import { type RO } from '@/utils/meta';
interface ToolbarConstituentsProps { interface ToolbarSchemaProps {
schema: IRSForm; schema: IRSForm;
isMutable: boolean; isMutable: boolean;
activeCst: IConstituenta | null; activeCst: IConstituenta | null;
@ -34,7 +34,7 @@ interface ToolbarConstituentsProps {
className?: string; className?: string;
} }
export function ToolbarConstituents({ export function ToolbarSchema({
schema, schema,
activeCst, activeCst,
setActive, setActive,
@ -42,7 +42,7 @@ export function ToolbarConstituents({
onEditActive, onEditActive,
isMutable, isMutable,
className className
}: ToolbarConstituentsProps) { }: ToolbarSchemaProps) {
const router = useConceptNavigation(); const router = useConceptNavigation();
const isProcessing = useMutatingRSForm(); const isProcessing = useMutatingRSForm();
const searchText = useCstSearchStore(state => state.query); const searchText = useCstSearchStore(state => state.query);
@ -175,6 +175,10 @@ export function ToolbarConstituents({
showTypeGraph({ items: typeInfo }); showTypeGraph({ items: typeInfo });
} }
function handleShowTermGraph() {
showTermGraph({ schema: schema });
}
return ( return (
<div className={cn('flex gap-0.5', className)}> <div className={cn('flex gap-0.5', className)}>
<MiniButton <MiniButton
@ -225,7 +229,7 @@ export function ToolbarConstituents({
<MiniButton <MiniButton
icon={<IconTree size='1rem' className='hover:text-primary' />} icon={<IconTree size='1rem' className='hover:text-primary' />}
title='Граф термов' title='Граф термов'
onClick={() => showTermGraph({ schema })} onClick={handleShowTermGraph}
/> />
<MiniButton <MiniButton
icon={<IconTypeGraph size='1rem' className='hover:text-primary' />} icon={<IconTypeGraph size='1rem' className='hover:text-primary' />}

View File

@ -8,7 +8,7 @@ import { ViewConstituents } from '@/features/rsform/components/view-constituents
import { useFitHeight } from '@/stores/app-layout'; import { useFitHeight } from '@/stores/app-layout';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { ToolbarConstituents } from './toolbar-constituents'; import { ToolbarSchema } from './toolbar-schema';
interface ViewSchemaProps { interface ViewSchemaProps {
schemaID: number; schemaID: number;
@ -29,7 +29,7 @@ export function ViewSchema({ schemaID, isMutable }: ViewSchemaProps) {
return ( return (
<div className='grid h-full relative cc-fade-in mt-5' style={{ gridTemplateRows: '1fr auto' }}> <div className='grid h-full relative cc-fade-in mt-5' style={{ gridTemplateRows: '1fr auto' }}>
<ToolbarConstituents <ToolbarSchema
className='absolute -top-6.5 left-1' className='absolute -top-6.5 left-1'
schema={schema} schema={schema}
activeCst={activeCst} activeCst={activeCst}

View File

@ -3,6 +3,8 @@
import { Handle, Position } from 'reactflow'; import { Handle, Position } from 'reactflow';
import clsx from 'clsx'; import clsx from 'clsx';
import { isBasicConcept } from '@/features/rsform/models/rsform-api';
import { APP_COLORS } from '@/styling/colors'; import { APP_COLORS } from '@/styling/colors';
import { globalIDs } from '@/utils/constants'; import { globalIDs } from '@/utils/constants';
@ -67,5 +69,9 @@ export function TGNode(node: TGNodeInternal) {
// ====== INTERNAL ====== // ====== INTERNAL ======
function describeCstNode(cst: IConstituenta) { function describeCstNode(cst: IConstituenta) {
return `${cst.alias}: ${cst.term_resolved}</br>Типизация: ${labelCstTypification(cst)}`; return `${cst.alias}: ${cst.term_resolved}</br><b>Типизация:</b> ${labelCstTypification(
cst
)}</br><b>Содержание:</b> ${
isBasicConcept(cst.cst_type) ? cst.convention : cst.definition_resolved || cst.definition_formal || cst.convention
}`;
} }

View File

@ -4,27 +4,19 @@ import { MiniButton } from '@/components/control';
import { IconGraphInputs, IconGraphOutputs, IconReset } from '@/components/icons'; import { IconGraphInputs, IconGraphOutputs, IconReset } from '@/components/icons';
import { cn } from '@/components/utils'; import { cn } from '@/components/utils';
import { useTermGraphStore } from '../../stores/term-graph';
interface ToolbarFocusedCstProps { interface ToolbarFocusedCstProps {
className?: string; className?: string;
focus: IConstituenta | null; focus: IConstituenta | null;
resetFocus: () => void; resetFocus: () => void;
showInputs: boolean;
toggleShowInputs: () => void;
showOutputs: boolean;
toggleShowOutputs: () => void;
} }
export function ToolbarFocusedCst({ export function ToolbarFocusedCst({ focus, resetFocus, className }: ToolbarFocusedCstProps) {
focus, const filter = useTermGraphStore(state => state.filter);
resetFocus, const toggleFocusInputs = useTermGraphStore(state => state.toggleFocusInputs);
className, const toggleFocusOutputs = useTermGraphStore(state => state.toggleFocusOutputs);
showInputs,
toggleShowInputs,
showOutputs,
toggleShowOutputs
}: ToolbarFocusedCstProps) {
if (!focus) { if (!focus) {
return null; return null;
} }
@ -44,14 +36,14 @@ export function ToolbarFocusedCst({
onClick={resetFocus} onClick={resetFocus}
/> />
<MiniButton <MiniButton
title={showInputs ? 'Скрыть поставщиков' : 'Отобразить поставщиков'} title={filter.focusShowInputs ? 'Скрыть поставщиков' : 'Отобразить поставщиков'}
icon={<IconGraphInputs size='1.25rem' className={showInputs ? 'icon-green' : 'icon-primary'} />} icon={<IconGraphInputs size='1.25rem' className={filter.focusShowInputs ? 'icon-green' : 'icon-primary'} />}
onClick={toggleShowInputs} onClick={toggleFocusInputs}
/> />
<MiniButton <MiniButton
title={showOutputs ? 'Скрыть потребителей' : 'Отобразить потребителей'} title={filter.focusShowOutputs ? 'Скрыть потребителей' : 'Отобразить потребителей'}
icon={<IconGraphOutputs size='1.25rem' className={showOutputs ? 'icon-green' : 'icon-primary'} />} icon={<IconGraphOutputs size='1.25rem' className={filter.focusShowOutputs ? 'icon-green' : 'icon-primary'} />}
onClick={toggleShowOutputs} onClick={toggleFocusOutputs}
/> />
</div> </div>
); );

View File

@ -55,8 +55,6 @@ export function TGFlow() {
const [edges, setEdges] = useEdgesState<Edge>([]); const [edges, setEdges] = useEdgesState<Edge>([]);
const filter = useTermGraphStore(state => state.filter); const filter = useTermGraphStore(state => state.filter);
const toggleFocusInputs = useTermGraphStore(state => state.toggleFocusInputs);
const toggleFocusOutputs = useTermGraphStore(state => state.toggleFocusOutputs);
const { filteredGraph, hidden } = useFilteredGraph(); const { filteredGraph, hidden } = useFilteredGraph();
function onSelectionChange({ nodes }: { nodes: Node[] }) { function onSelectionChange({ nodes }: { nodes: Node[] }) {
@ -170,14 +168,7 @@ export function TGFlow() {
<div className='relative' tabIndex={-1} onKeyDown={handleKeyDown}> <div className='relative' tabIndex={-1} onKeyDown={handleKeyDown}>
<div className='cc-tab-tools flex flex-col items-center rounded-b-2xl backdrop-blur-xs'> <div className='cc-tab-tools flex flex-col items-center rounded-b-2xl backdrop-blur-xs'>
<ToolbarTermGraph /> <ToolbarTermGraph />
<ToolbarFocusedCst <ToolbarFocusedCst focus={focusCst} resetFocus={() => setFocus(null)} />
focus={focusCst}
resetFocus={() => setFocus(null)}
showInputs={filter.focusShowInputs}
toggleShowInputs={toggleFocusInputs}
showOutputs={filter.focusShowOutputs}
toggleShowOutputs={toggleFocusOutputs}
/>
{!focusCst ? ( {!focusCst ? (
<ToolbarGraphSelection <ToolbarGraphSelection
graph={schema.graph} graph={schema.graph}

View File

@ -43,7 +43,8 @@ export function ToolbarTermGraph() {
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph); const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
const showParams = useDialogsStore(state => state.showGraphParams); const showParams = useDialogsStore(state => state.showGraphParams);
const filter = useTermGraphStore(state => state.filter); 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(); const { fitView } = useReactFlow();
@ -68,13 +69,6 @@ export function ToolbarTermGraph() {
promptDeleteCst(); promptDeleteCst();
} }
function handleToggleNoText() {
setFilter({
...filter,
noText: !filter.noText
});
}
function handleFitView() { function handleFitView() {
setTimeout(() => { setTimeout(() => {
fitView(flowOptions.fitViewOptions); fitView(flowOptions.fitViewOptions);
@ -88,13 +82,6 @@ export function ToolbarTermGraph() {
} }
} }
function handleFoldDerived() {
setFilter({
...filter,
foldDerived: !filter.foldDerived
});
}
function handleSelectOss(event: React.MouseEvent<HTMLElement>, newValue: ILibraryItemReference) { function handleSelectOss(event: React.MouseEvent<HTMLElement>, newValue: ILibraryItemReference) {
navigateOss(newValue.id, event.ctrlKey || event.metaKey); navigateOss(newValue.id, event.ctrlKey || event.metaKey);
} }
@ -127,7 +114,7 @@ export function ToolbarTermGraph() {
<IconTextOff size='1.25rem' className='icon-primary' /> <IconTextOff size='1.25rem' className='icon-primary' />
) )
} }
onClick={handleToggleNoText} onClick={toggleText}
/> />
<MiniButton <MiniButton
title={!filter.foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные'} title={!filter.foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные'}
@ -138,7 +125,7 @@ export function ToolbarTermGraph() {
<IconClusteringOff size='1.25rem' className='icon-primary' /> <IconClusteringOff size='1.25rem' className='icon-primary' />
) )
} }
onClick={handleFoldDerived} onClick={toggleClustering}
/> />
{isContentEditable ? ( {isContentEditable ? (
<MiniButton <MiniButton

View File

@ -34,6 +34,8 @@ interface TermGraphStore {
setFilter: (value: GraphFilterParams) => void; setFilter: (value: GraphFilterParams) => void;
toggleFocusInputs: () => void; toggleFocusInputs: () => void;
toggleFocusOutputs: () => void; toggleFocusOutputs: () => void;
toggleText: () => void;
toggleClustering: () => void;
foldHidden: boolean; foldHidden: boolean;
toggleFoldHidden: () => void; toggleFoldHidden: () => void;
@ -69,6 +71,8 @@ export const useTermGraphStore = create<TermGraphStore>()(
set(state => ({ filter: { ...state.filter, focusShowInputs: !state.filter.focusShowInputs } })), set(state => ({ filter: { ...state.filter, focusShowInputs: !state.filter.focusShowInputs } })),
toggleFocusOutputs: () => toggleFocusOutputs: () =>
set(state => ({ filter: { ...state.filter, focusShowOutputs: !state.filter.focusShowOutputs } })), 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, foldHidden: false,
toggleFoldHidden: () => set(state => ({ foldHidden: !state.foldHidden })), toggleFoldHidden: () => set(state => ({ foldHidden: !state.foldHidden })),

View File

@ -39,31 +39,31 @@ export const DialogType = {
RENAME_CONSTITUENTA: 6, RENAME_CONSTITUENTA: 6,
CREATE_BLOCK: 7, CREATE_BLOCK: 7,
EDIT_BLOCK: 25, EDIT_BLOCK: 8,
CREATE_OPERATION: 8, CREATE_OPERATION: 9,
EDIT_OPERATION: 9, EDIT_OPERATION: 10,
DELETE_OPERATION: 10, DELETE_OPERATION: 11,
CHANGE_INPUT_SCHEMA: 11, CHANGE_INPUT_SCHEMA: 12,
RELOCATE_CONSTITUENTS: 12, RELOCATE_CONSTITUENTS: 13,
OSS_SETTINGS: 26, OSS_SETTINGS: 14,
EDIT_CONSTITUENTA: 27, EDIT_CONSTITUENTA: 15,
CLONE_LIBRARY_ITEM: 13, CLONE_LIBRARY_ITEM: 16,
UPLOAD_RSFORM: 14, UPLOAD_RSFORM: 17,
EDIT_EDITORS: 15, EDIT_EDITORS: 18,
EDIT_VERSIONS: 16, EDIT_VERSIONS: 19,
CHANGE_LOCATION: 17, CHANGE_LOCATION: 20,
EDIT_REFERENCE: 18, EDIT_REFERENCE: 21,
EDIT_WORD_FORMS: 19, EDIT_WORD_FORMS: 22,
INLINE_SYNTHESIS: 20, INLINE_SYNTHESIS: 23,
SHOW_QR_CODE: 21, SHOW_QR_CODE: 24,
SHOW_AST: 22, SHOW_AST: 25,
SHOW_TYPE_GRAPH: 23, SHOW_TYPE_GRAPH: 26,
GRAPH_PARAMETERS: 24, GRAPH_PARAMETERS: 27,
SHOW_TERM_GRAPH: 25 SHOW_TERM_GRAPH: 28
} as const; } as const;
export type DialogType = (typeof DialogType)[keyof typeof DialogType]; export type DialogType = (typeof DialogType)[keyof typeof DialogType];