R: Refactor term-graph preparing to extract shared components
This commit is contained in:
parent
d901094d8e
commit
315c0f847e
|
@ -0,0 +1,52 @@
|
||||||
|
// Dialog for read-only display of the TermGraph for OSS. Currently ignores activeCst and only shows the schema graph.
|
||||||
|
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';
|
||||||
|
import { IconRSForm } from '@/components/icons';
|
||||||
|
import { ModalView } from '@/components/modal';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
|
export interface DlgShowTermGraphProps {
|
||||||
|
schema: IRSForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DlgShowTermGraph() {
|
||||||
|
const { schema } = useDialogsStore(state => state.props as DlgShowTermGraphProps);
|
||||||
|
const hideDialog = useDialogsStore(state => state.hideDialog);
|
||||||
|
const router = useConceptNavigation();
|
||||||
|
|
||||||
|
function navigateToSchema() {
|
||||||
|
hideDialog();
|
||||||
|
router.push({
|
||||||
|
path: urls.schema_props({
|
||||||
|
id: schema.id,
|
||||||
|
tab: RSTabID.GRAPH
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalView
|
||||||
|
className='relative w-[calc(100dvw-3rem)] h-[calc(100dvh-3rem)] cc-mask-sides'
|
||||||
|
fullScreen
|
||||||
|
header='Граф термов'
|
||||||
|
>
|
||||||
|
<MiniButton
|
||||||
|
title='Открыть концептуальную схему'
|
||||||
|
noPadding
|
||||||
|
icon={<IconRSForm size='1.25rem' className='text-primary' />}
|
||||||
|
onClick={navigateToSchema}
|
||||||
|
/>
|
||||||
|
<ReactFlowProvider>
|
||||||
|
{/* TGFlow expects schema from context, so you may need to refactor TGFlow to accept schema as prop if needed */}
|
||||||
|
<TGFlow />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
</ModalView>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './dlg-show-term-graph';
|
|
@ -17,7 +17,6 @@ import { OssFlowContext } from './oss-flow-context';
|
||||||
const Z_BLOCK = 1;
|
const Z_BLOCK = 1;
|
||||||
const Z_SCHEMA = 10;
|
const Z_SCHEMA = 10;
|
||||||
|
|
||||||
// TODO: decouple nodes and edges from controller callbacks
|
|
||||||
export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
||||||
const { schema, setSelected } = useOssEdit();
|
const { schema, setSelected } = useOssEdit();
|
||||||
const { fitView } = useReactFlow();
|
const { fitView } = useReactFlow();
|
||||||
|
@ -77,6 +76,7 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
||||||
target: target.nodeID,
|
target: target.nodeID,
|
||||||
type: edgeStraight ? 'straight' : 'simplebezier',
|
type: edgeStraight ? 'straight' : 'simplebezier',
|
||||||
animated: edgeAnimate,
|
animated: edgeAnimate,
|
||||||
|
focusable: false,
|
||||||
targetHandle: source.x > target.x ? 'right' : 'left'
|
targetHandle: source.x > target.x ? 'right' : 'left'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,7 +23,6 @@ import { cn } from '@/components/utils';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
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';
|
||||||
import { notImplemented } from '@/utils/utils';
|
|
||||||
|
|
||||||
interface ToolbarConstituentsProps {
|
interface ToolbarConstituentsProps {
|
||||||
schema: IRSForm;
|
schema: IRSForm;
|
||||||
|
@ -52,6 +51,7 @@ export function ToolbarConstituents({
|
||||||
const showCreateCst = useDialogsStore(state => state.showCreateCst);
|
const showCreateCst = useDialogsStore(state => state.showCreateCst);
|
||||||
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
||||||
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
|
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
|
||||||
|
const showTermGraph = useDialogsStore(state => state.showShowTermGraph);
|
||||||
const { moveConstituents } = useMoveConstituents();
|
const { moveConstituents } = useMoveConstituents();
|
||||||
const { createConstituenta } = useCreateConstituenta();
|
const { createConstituenta } = useCreateConstituenta();
|
||||||
|
|
||||||
|
@ -225,7 +225,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={notImplemented}
|
onClick={() => showTermGraph({ schema })}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<IconTypeGraph size='1rem' className='hover:text-primary' />}
|
icon={<IconTypeGraph size='1rem' className='hover:text-primary' />}
|
||||||
|
|
|
@ -92,7 +92,6 @@ export function DlgEditCst() {
|
||||||
title='Редактировать в КС'
|
title='Редактировать в КС'
|
||||||
noPadding
|
noPadding
|
||||||
icon={<IconRSForm size='1.25rem' className='text-primary' />}
|
icon={<IconRSForm size='1.25rem' className='text-primary' />}
|
||||||
className=''
|
|
||||||
onClick={navigateToTarget}
|
onClick={navigateToTarget}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
|
|
@ -1,11 +1,35 @@
|
||||||
|
/**
|
||||||
|
* Module: Graph of Terms graphical representation.
|
||||||
|
*/
|
||||||
import { type Edge, type Node } from 'reactflow';
|
import { type Edge, type Node } from 'reactflow';
|
||||||
import dagre from '@dagrejs/dagre';
|
import dagre from '@dagrejs/dagre';
|
||||||
|
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import { type IConstituenta } from '../../../../models/rsform';
|
import { type IConstituenta } from './rsform';
|
||||||
|
|
||||||
export function applyLayout(nodes: Node<IConstituenta>[], edges: Edge[], subLabels: boolean) {
|
export interface TGNodeState {
|
||||||
|
cst: IConstituenta;
|
||||||
|
focused: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Represents graph node. */
|
||||||
|
export interface TGNodeData extends Node {
|
||||||
|
id: string;
|
||||||
|
data: TGNodeState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Represents graph node internal data. */
|
||||||
|
export interface TGNodeInternal {
|
||||||
|
id: string;
|
||||||
|
data: TGNodeState;
|
||||||
|
selected: boolean;
|
||||||
|
dragging: boolean;
|
||||||
|
xPos: number;
|
||||||
|
yPos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyLayout(nodes: Node<TGNodeState>[], edges: Edge[], subLabels: boolean) {
|
||||||
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||||
dagreGraph.setGraph({
|
dagreGraph.setGraph({
|
||||||
rankdir: 'TB',
|
rankdir: 'TB',
|
|
@ -277,7 +277,7 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
|
||||||
id='cst_convention'
|
id='cst_convention'
|
||||||
{...register('item_data.convention')}
|
{...register('item_data.convention')}
|
||||||
fitContent
|
fitContent
|
||||||
className='max-h-32'
|
className='disabled:min-h-9 max-h-32'
|
||||||
spellCheck
|
spellCheck
|
||||||
label={isBasic ? 'Конвенция' : 'Комментарий'}
|
label={isBasic ? 'Конвенция' : 'Комментарий'}
|
||||||
placeholder={disabled ? '' : isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
|
placeholder={disabled ? '' : isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
|
||||||
|
|
|
@ -8,45 +8,19 @@ import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { colorBgGraphNode } from '../../../../colors';
|
import { colorBgGraphNode } from '../../../../colors';
|
||||||
import { labelCstTypification } from '../../../../labels';
|
import { labelCstTypification } from '../../../../labels';
|
||||||
|
import { type TGNodeInternal } from '../../../../models/graph-layout';
|
||||||
import { type IConstituenta } from '../../../../models/rsform';
|
import { type IConstituenta } from '../../../../models/rsform';
|
||||||
import { useTermGraphStore } from '../../../../stores/term-graph';
|
import { useTermGraphStore } from '../../../../stores/term-graph';
|
||||||
import { useRSEdit } from '../../rsedit-context';
|
|
||||||
|
|
||||||
const DESCRIPTION_THRESHOLD = 15;
|
const DESCRIPTION_THRESHOLD = 15;
|
||||||
const LABEL_THRESHOLD = 3;
|
const LABEL_THRESHOLD = 3;
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents graph AST node internal data.
|
|
||||||
*/
|
|
||||||
interface TGNodeInternal {
|
|
||||||
id: string;
|
|
||||||
data: IConstituenta;
|
|
||||||
selected: boolean;
|
|
||||||
dragging: boolean;
|
|
||||||
xPos: number;
|
|
||||||
yPos: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TGNode(node: TGNodeInternal) {
|
export function TGNode(node: TGNodeInternal) {
|
||||||
const { focusCst, setFocus, navigateCst } = useRSEdit();
|
|
||||||
const filter = useTermGraphStore(state => state.filter);
|
const filter = useTermGraphStore(state => state.filter);
|
||||||
const coloring = useTermGraphStore(state => state.coloring);
|
const coloring = useTermGraphStore(state => state.coloring);
|
||||||
const isFocused = focusCst?.id === node.data.id;
|
|
||||||
|
|
||||||
const label = node.data.alias;
|
const label = node.data.cst.alias;
|
||||||
const description = !filter.noText ? node.data.term_resolved : '';
|
const description = !filter.noText ? node.data.cst.term_resolved : '';
|
||||||
|
|
||||||
function handleContextMenu(event: React.MouseEvent<HTMLElement>) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
setFocus(isFocused ? null : node.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDoubleClick(event: React.MouseEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
navigateCst(node.data.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -54,21 +28,19 @@ export function TGNode(node: TGNodeInternal) {
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full h-full cursor-default flex items-center justify-center rounded-full',
|
'w-full h-full cursor-default flex items-center justify-center rounded-full',
|
||||||
isFocused && 'border-[2px] border-selected',
|
node.data.focused && 'border-[2px] border-selected',
|
||||||
label.length > LABEL_THRESHOLD ? 'text-[12px]/[16px]' : 'text-[14px]/[20px]'
|
label.length > LABEL_THRESHOLD ? 'text-[12px]/[16px]' : 'text-[14px]/[20px]'
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: node.selected
|
backgroundColor: node.selected
|
||||||
? APP_COLORS.bgActiveSelection
|
? APP_COLORS.bgActiveSelection
|
||||||
: isFocused
|
: node.data.focused
|
||||||
? APP_COLORS.bgPurple
|
? APP_COLORS.bgPurple
|
||||||
: colorBgGraphNode(node.data, coloring)
|
: colorBgGraphNode(node.data.cst, coloring)
|
||||||
}}
|
}}
|
||||||
data-tooltip-id={globalIDs.tooltip}
|
data-tooltip-id={globalIDs.tooltip}
|
||||||
data-tooltip-html={describeCstNode(node.data)}
|
data-tooltip-html={describeCstNode(node.data.cst)}
|
||||||
data-tooltip-hidden={node.dragging}
|
data-tooltip-hidden={node.dragging}
|
||||||
onContextMenu={handleContextMenu}
|
|
||||||
onDoubleClick={handleDoubleClick}
|
|
||||||
>
|
>
|
||||||
<div className='cc-node-label'>{label}</div>
|
<div className='cc-node-label'>{label}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -80,8 +52,6 @@ export function TGNode(node: TGNodeInternal) {
|
||||||
'pointer-events-none',
|
'pointer-events-none',
|
||||||
description.length > DESCRIPTION_THRESHOLD ? 'text-[10px]/[12px]' : 'text-[12px]/[16px]'
|
description.length > DESCRIPTION_THRESHOLD ? 'text-[10px]/[12px]' : 'text-[12px]/[16px]'
|
||||||
)}
|
)}
|
||||||
onContextMenu={handleContextMenu}
|
|
||||||
onDoubleClick={handleDoubleClick}
|
|
||||||
>
|
>
|
||||||
<div className='absolute top-0 px-[4px] left-0 text-center w-full line-clamp-3 hover:line-clamp-none'>
|
<div className='absolute top-0 px-[4px] left-0 text-center w-full line-clamp-3 hover:line-clamp-none'>
|
||||||
{description}
|
{description}
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
import { useLibrary } from '@/features/library/backend/use-library';
|
import { useLibrary } from '@/features/library/backend/use-library';
|
||||||
|
import { type IRSForm } from '@/features/rsform/models/rsform';
|
||||||
|
|
||||||
import { Tooltip } from '@/components/container';
|
import { Tooltip } from '@/components/container';
|
||||||
import { IconHelp } from '@/components/icons';
|
import { IconHelp } from '@/components/icons';
|
||||||
import { globalIDs, prefixes } from '@/utils/constants';
|
import { globalIDs, prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
import { colorBgSchemas } from '../../../colors';
|
import { colorBgSchemas } from '../../../colors';
|
||||||
import { useRSEdit } from '../rsedit-context';
|
|
||||||
|
|
||||||
export function SchemasGuide() {
|
interface SchemasGuideProps {
|
||||||
|
schema: IRSForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SchemasGuide({ schema }: SchemasGuideProps) {
|
||||||
const libraryItems = useLibrary().items;
|
const libraryItems = useLibrary().items;
|
||||||
const schema = useRSEdit().schema;
|
|
||||||
|
|
||||||
const schemas = (() => {
|
const schemas = (() => {
|
||||||
const processed = new Set<number>();
|
const processed = new Set<number>();
|
||||||
|
|
|
@ -1,23 +1,32 @@
|
||||||
import { HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
import { BadgeHelp } from '@/features/help/components/badge-help';
|
||||||
|
import { type IRSForm } from '@/features/rsform/models/rsform';
|
||||||
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/input/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/input/select';
|
||||||
|
import { cn } from '@/components/utils';
|
||||||
|
|
||||||
import { mapLabelColoring } from '../../../labels';
|
import { mapLabelColoring } from '../../../labels';
|
||||||
import { useTermGraphStore } from '../../../stores/term-graph';
|
import { useTermGraphStore } from '../../../stores/term-graph';
|
||||||
|
|
||||||
import { SchemasGuide } from './schemas-guide';
|
import { SchemasGuide } from './schemas-guide';
|
||||||
|
|
||||||
export function SelectColoring() {
|
interface SelectColoringProps {
|
||||||
|
className?: string;
|
||||||
|
schema: IRSForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectColoring({ className, schema }: SelectColoringProps) {
|
||||||
const coloring = useTermGraphStore(state => state.coloring);
|
const coloring = useTermGraphStore(state => state.coloring);
|
||||||
const setColoring = useTermGraphStore(state => state.setColoring);
|
const setColoring = useTermGraphStore(state => state.setColoring);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative border rounded-b-none select-none bg-input rounded-t-md pointer-events-auto'>
|
<div
|
||||||
|
className={cn('relative border rounded-b-none select-none bg-input rounded-t-md pointer-events-auto', className)}
|
||||||
|
>
|
||||||
<div className='absolute z-pop right-10 h-9 flex items-center'>
|
<div className='absolute z-pop right-10 h-9 flex items-center'>
|
||||||
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} contentClass='min-w-100' /> : null}
|
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} contentClass='min-w-100' /> : null}
|
||||||
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} contentClass='min-w-100' /> : null}
|
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} contentClass='min-w-100' /> : null}
|
||||||
{coloring === 'schemas' ? <SchemasGuide /> : null}
|
{coloring === 'schemas' ? <SchemasGuide schema={schema} /> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Select onValueChange={setColoring} defaultValue={coloring}>
|
<Select onValueChange={setColoring} defaultValue={coloring}>
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { type Edge, MarkerType, type Node, useEdgesState, useNodesState, useOnSelectionChange } from 'reactflow';
|
import { type Edge, MarkerType, type Node, useEdgesState, useNodesState, useOnSelectionChange } from 'reactflow';
|
||||||
|
|
||||||
|
import { applyLayout, type TGNodeData } from '@/features/rsform/models/graph-layout';
|
||||||
|
|
||||||
import { DiagramFlow, useReactFlow, useStoreApi } from '@/components/flow/diagram-flow';
|
import { DiagramFlow, useReactFlow, useStoreApi } from '@/components/flow/diagram-flow';
|
||||||
import { useMainHeight } from '@/stores/app-layout';
|
import { useMainHeight } from '@/stores/app-layout';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
@ -10,13 +12,11 @@ import { withPreventDefault } from '@/utils/utils';
|
||||||
|
|
||||||
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
|
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
|
||||||
import { ToolbarGraphSelection } from '../../../components/toolbar-graph-selection';
|
import { ToolbarGraphSelection } from '../../../components/toolbar-graph-selection';
|
||||||
import { type IConstituenta } from '../../../models/rsform';
|
|
||||||
import { isBasicConcept } from '../../../models/rsform-api';
|
import { isBasicConcept } from '../../../models/rsform-api';
|
||||||
import { useTermGraphStore } from '../../../stores/term-graph';
|
import { useTermGraphStore } from '../../../stores/term-graph';
|
||||||
import { useRSEdit } from '../rsedit-context';
|
import { useRSEdit } from '../rsedit-context';
|
||||||
|
|
||||||
import { TGEdgeTypes } from './graph/tg-edge-types';
|
import { TGEdgeTypes } from './graph/tg-edge-types';
|
||||||
import { applyLayout } from './graph/tg-layout';
|
|
||||||
import { TGNodeTypes } from './graph/tg-node-types';
|
import { TGNodeTypes } from './graph/tg-node-types';
|
||||||
import { SelectColoring } from './select-coloring';
|
import { SelectColoring } from './select-coloring';
|
||||||
import { ToolbarFocusedCst } from './toolbar-focused-cst';
|
import { ToolbarFocusedCst } from './toolbar-focused-cst';
|
||||||
|
@ -40,13 +40,24 @@ export function TGFlow() {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const { addSelectedNodes } = store.getState();
|
const { addSelectedNodes } = store.getState();
|
||||||
const isProcessing = useMutatingRSForm();
|
const isProcessing = useMutatingRSForm();
|
||||||
const { isContentEditable, schema, selected, setSelected, promptDeleteCst, focusCst, setFocus, deselectAll } =
|
const {
|
||||||
useRSEdit();
|
isContentEditable,
|
||||||
|
schema,
|
||||||
|
selected,
|
||||||
|
setSelected,
|
||||||
|
promptDeleteCst,
|
||||||
|
focusCst,
|
||||||
|
setFocus,
|
||||||
|
deselectAll,
|
||||||
|
navigateCst
|
||||||
|
} = useRSEdit();
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
||||||
const [edges, setEdges] = useEdgesState([]);
|
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[] }) {
|
||||||
|
@ -65,7 +76,7 @@ export function TGFlow() {
|
||||||
if (!viewportInitialized) {
|
if (!viewportInitialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newNodes: Node<IConstituenta>[] = [];
|
const newNodes: Node[] = [];
|
||||||
filteredGraph.nodes.forEach(node => {
|
filteredGraph.nodes.forEach(node => {
|
||||||
const cst = schema.cstByID.get(node.id);
|
const cst = schema.cstByID.get(node.id);
|
||||||
if (cst) {
|
if (cst) {
|
||||||
|
@ -73,7 +84,7 @@ export function TGFlow() {
|
||||||
id: String(node.id),
|
id: String(node.id),
|
||||||
type: 'concept',
|
type: 'concept',
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
data: cst
|
data: { cst: cst, focused: focusCst?.id === cst.id }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -144,11 +155,30 @@ export function TGFlow() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleNodeContextMenu(event: React.MouseEvent<Element>, node: TGNodeData) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setFocus(focusCst?.id === node.data.cst.id ? null : node.data.cst);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: TGNodeData) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
navigateCst(node.data.cst.id);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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)}
|
||||||
|
showInputs={filter.focusShowInputs}
|
||||||
|
toggleShowInputs={toggleFocusInputs}
|
||||||
|
showOutputs={filter.focusShowOutputs}
|
||||||
|
toggleShowOutputs={toggleFocusOutputs}
|
||||||
|
/>
|
||||||
{!focusCst ? (
|
{!focusCst ? (
|
||||||
<ToolbarGraphSelection
|
<ToolbarGraphSelection
|
||||||
graph={schema.graph}
|
graph={schema.graph}
|
||||||
|
@ -167,7 +197,7 @@ export function TGFlow() {
|
||||||
<span className='px-2 pb-1 select-none whitespace-nowrap backdrop-blur-xs rounded-xl w-fit'>
|
<span className='px-2 pb-1 select-none whitespace-nowrap backdrop-blur-xs rounded-xl w-fit'>
|
||||||
Выбор {selected.length} из {schema.stats?.count_all ?? 0}
|
Выбор {selected.length} из {schema.stats?.count_all ?? 0}
|
||||||
</span>
|
</span>
|
||||||
<SelectColoring />
|
<SelectColoring schema={schema} />
|
||||||
<ViewHidden items={hidden} />
|
<ViewHidden items={hidden} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -180,6 +210,8 @@ export function TGFlow() {
|
||||||
nodeTypes={TGNodeTypes}
|
nodeTypes={TGNodeTypes}
|
||||||
edgeTypes={TGEdgeTypes}
|
edgeTypes={TGEdgeTypes}
|
||||||
onContextMenu={event => event.preventDefault()}
|
onContextMenu={event => event.preventDefault()}
|
||||||
|
onNodeContextMenu={handleNodeContextMenu}
|
||||||
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,45 +1,40 @@
|
||||||
'use client';
|
import { type IConstituenta } from '@/features/rsform/models/rsform';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
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 { useTermGraphStore } from '../../../stores/term-graph';
|
interface ToolbarFocusedCstProps {
|
||||||
import { useRSEdit } from '../rsedit-context';
|
className?: string;
|
||||||
|
focus: IConstituenta | null;
|
||||||
|
resetFocus: () => void;
|
||||||
|
|
||||||
export function ToolbarFocusedCst() {
|
showInputs: boolean;
|
||||||
const { setFocus, focusCst } = useRSEdit();
|
toggleShowInputs: () => void;
|
||||||
|
|
||||||
const filter = useTermGraphStore(state => state.filter);
|
showOutputs: boolean;
|
||||||
const setFilter = useTermGraphStore(state => state.setFilter);
|
toggleShowOutputs: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
function resetFocus() {
|
export function ToolbarFocusedCst({
|
||||||
setFocus(null);
|
focus,
|
||||||
}
|
resetFocus,
|
||||||
|
className,
|
||||||
function handleShowInputs() {
|
showInputs,
|
||||||
setFilter({
|
toggleShowInputs,
|
||||||
...filter,
|
showOutputs,
|
||||||
focusShowInputs: !filter.focusShowInputs
|
toggleShowOutputs
|
||||||
});
|
}: ToolbarFocusedCstProps) {
|
||||||
}
|
if (!focus) {
|
||||||
|
|
||||||
function handleShowOutputs() {
|
|
||||||
setFilter({
|
|
||||||
...filter,
|
|
||||||
focusShowOutputs: !filter.focusShowOutputs
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!focusCst) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center cc-icons'>
|
<div className={cn('flex items-center cc-icons', className)}>
|
||||||
<div className='w-31 mt-0.5 text-right select-none text-(--acc-fg-purple)'>
|
<div className='w-31 mt-0.5 text-right select-none text-(--acc-fg-purple)'>
|
||||||
<span>
|
<span>
|
||||||
Фокус
|
Фокус
|
||||||
<b> {focusCst.alias} </b>
|
<b> {focus.alias} </b>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -49,14 +44,14 @@ export function ToolbarFocusedCst() {
|
||||||
onClick={resetFocus}
|
onClick={resetFocus}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={filter.focusShowInputs ? 'Скрыть поставщиков' : 'Отобразить поставщиков'}
|
title={showInputs ? 'Скрыть поставщиков' : 'Отобразить поставщиков'}
|
||||||
icon={<IconGraphInputs size='1.25rem' className={filter.focusShowInputs ? 'icon-green' : 'icon-primary'} />}
|
icon={<IconGraphInputs size='1.25rem' className={showInputs ? 'icon-green' : 'icon-primary'} />}
|
||||||
onClick={handleShowInputs}
|
onClick={toggleShowInputs}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={filter.focusShowOutputs ? 'Скрыть потребителей' : 'Отобразить потребителей'}
|
title={showOutputs ? 'Скрыть потребителей' : 'Отобразить потребителей'}
|
||||||
icon={<IconGraphOutputs size='1.25rem' className={filter.focusShowOutputs ? 'icon-green' : 'icon-primary'} />}
|
icon={<IconGraphOutputs size='1.25rem' className={showOutputs ? 'icon-green' : 'icon-primary'} />}
|
||||||
onClick={handleShowOutputs}
|
onClick={toggleShowOutputs}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function useFilteredGraph() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== Internals =========
|
// ====== Internals =========
|
||||||
function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, focusCst: IConstituenta | null) {
|
export function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, focusCst: IConstituenta | null) {
|
||||||
const filtered = schema.graph.clone();
|
const filtered = schema.graph.clone();
|
||||||
const allowedTypes: CstType[] = (() => {
|
const allowedTypes: CstType[] = (() => {
|
||||||
const result: CstType[] = [];
|
const result: CstType[] = [];
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const RSTabID = {
|
||||||
CARD: 0,
|
CARD: 0,
|
||||||
CST_LIST: 1,
|
CST_LIST: 1,
|
||||||
CST_EDIT: 2,
|
CST_EDIT: 2,
|
||||||
TERM_GRAPH: 3
|
GRAPH: 3
|
||||||
} as const;
|
} as const;
|
||||||
export type RSTabID = (typeof RSTabID)[keyof typeof RSTabID];
|
export type RSTabID = (typeof RSTabID)[keyof typeof RSTabID];
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,8 @@ export interface GraphFilterParams {
|
||||||
interface TermGraphStore {
|
interface TermGraphStore {
|
||||||
filter: GraphFilterParams;
|
filter: GraphFilterParams;
|
||||||
setFilter: (value: GraphFilterParams) => void;
|
setFilter: (value: GraphFilterParams) => void;
|
||||||
|
toggleFocusInputs: () => void;
|
||||||
|
toggleFocusOutputs: () => void;
|
||||||
|
|
||||||
foldHidden: boolean;
|
foldHidden: boolean;
|
||||||
toggleFoldHidden: () => void;
|
toggleFoldHidden: () => void;
|
||||||
|
@ -63,6 +65,10 @@ export const useTermGraphStore = create<TermGraphStore>()(
|
||||||
allowTheorem: true
|
allowTheorem: true
|
||||||
},
|
},
|
||||||
setFilter: value => set({ filter: value }),
|
setFilter: value => set({ filter: value }),
|
||||||
|
toggleFocusInputs: () =>
|
||||||
|
set(state => ({ filter: { ...state.filter, focusShowInputs: !state.filter.focusShowInputs } })),
|
||||||
|
toggleFocusOutputs: () =>
|
||||||
|
set(state => ({ filter: { ...state.filter, focusShowOutputs: !state.filter.focusShowOutputs } })),
|
||||||
|
|
||||||
foldHidden: false,
|
foldHidden: false,
|
||||||
toggleFoldHidden: () => set(state => ({ foldHidden: !state.foldHidden })),
|
toggleFoldHidden: () => set(state => ({ foldHidden: !state.foldHidden })),
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { type DlgDeleteOperationProps } from '@/features/oss/dialogs/dlg-delete-
|
||||||
import { type DlgEditBlockProps } from '@/features/oss/dialogs/dlg-edit-block';
|
import { type DlgEditBlockProps } from '@/features/oss/dialogs/dlg-edit-block';
|
||||||
import { type DlgEditOperationProps } from '@/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation';
|
import { type DlgEditOperationProps } from '@/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation';
|
||||||
import { type DlgRelocateConstituentsProps } from '@/features/oss/dialogs/dlg-relocate-constituents';
|
import { type DlgRelocateConstituentsProps } from '@/features/oss/dialogs/dlg-relocate-constituents';
|
||||||
|
import { type DlgShowTermGraphProps } from '@/features/oss/dialogs/dlg-show-term-graph/dlg-show-term-graph';
|
||||||
import { type DlgCreateCstProps } from '@/features/rsform/dialogs/dlg-create-cst/dlg-create-cst';
|
import { type DlgCreateCstProps } from '@/features/rsform/dialogs/dlg-create-cst/dlg-create-cst';
|
||||||
import { type DlgCstTemplateProps } from '@/features/rsform/dialogs/dlg-cst-template/dlg-cst-template';
|
import { type DlgCstTemplateProps } from '@/features/rsform/dialogs/dlg-cst-template/dlg-cst-template';
|
||||||
import { type DlgDeleteCstProps } from '@/features/rsform/dialogs/dlg-delete-cst/dlg-delete-cst';
|
import { type DlgDeleteCstProps } from '@/features/rsform/dialogs/dlg-delete-cst/dlg-delete-cst';
|
||||||
|
@ -61,7 +62,8 @@ export const DialogType = {
|
||||||
SHOW_QR_CODE: 21,
|
SHOW_QR_CODE: 21,
|
||||||
SHOW_AST: 22,
|
SHOW_AST: 22,
|
||||||
SHOW_TYPE_GRAPH: 23,
|
SHOW_TYPE_GRAPH: 23,
|
||||||
GRAPH_PARAMETERS: 24
|
GRAPH_PARAMETERS: 24,
|
||||||
|
SHOW_TERM_GRAPH: 25
|
||||||
} as const;
|
} as const;
|
||||||
export type DialogType = (typeof DialogType)[keyof typeof DialogType];
|
export type DialogType = (typeof DialogType)[keyof typeof DialogType];
|
||||||
|
|
||||||
|
@ -88,6 +90,7 @@ interface DialogsStore {
|
||||||
showInlineSynthesis: (props: DlgInlineSynthesisProps) => void;
|
showInlineSynthesis: (props: DlgInlineSynthesisProps) => void;
|
||||||
showShowAST: (props: DlgShowASTProps) => void;
|
showShowAST: (props: DlgShowASTProps) => void;
|
||||||
showShowTypeGraph: (props: DlgShowTypeGraphProps) => void;
|
showShowTypeGraph: (props: DlgShowTypeGraphProps) => void;
|
||||||
|
showShowTermGraph: (props: DlgShowTermGraphProps) => void;
|
||||||
showChangeInputSchema: (props: DlgChangeInputSchemaProps) => void;
|
showChangeInputSchema: (props: DlgChangeInputSchemaProps) => void;
|
||||||
showChangeLocation: (props: DlgChangeLocationProps) => void;
|
showChangeLocation: (props: DlgChangeLocationProps) => void;
|
||||||
showCloneLibraryItem: (props: DlgCloneLibraryItemProps) => void;
|
showCloneLibraryItem: (props: DlgCloneLibraryItemProps) => void;
|
||||||
|
@ -127,6 +130,7 @@ export const useDialogsStore = create<DialogsStore>()(set => ({
|
||||||
showInlineSynthesis: props => set({ active: DialogType.INLINE_SYNTHESIS, props: props }),
|
showInlineSynthesis: props => set({ active: DialogType.INLINE_SYNTHESIS, props: props }),
|
||||||
showShowAST: props => set({ active: DialogType.SHOW_AST, props: props }),
|
showShowAST: props => set({ active: DialogType.SHOW_AST, props: props }),
|
||||||
showShowTypeGraph: props => set({ active: DialogType.SHOW_TYPE_GRAPH, props: props }),
|
showShowTypeGraph: props => set({ active: DialogType.SHOW_TYPE_GRAPH, props: props }),
|
||||||
|
showShowTermGraph: props => set({ active: DialogType.SHOW_TERM_GRAPH, props: props }),
|
||||||
showChangeInputSchema: props => set({ active: DialogType.CHANGE_INPUT_SCHEMA, props: props }),
|
showChangeInputSchema: props => set({ active: DialogType.CHANGE_INPUT_SCHEMA, props: props }),
|
||||||
showChangeLocation: props => set({ active: DialogType.CHANGE_LOCATION, props: props }),
|
showChangeLocation: props => set({ active: DialogType.CHANGE_LOCATION, props: props }),
|
||||||
showCloneLibraryItem: props => set({ active: DialogType.CLONE_LIBRARY_ITEM, props: props }),
|
showCloneLibraryItem: props => set({ active: DialogType.CLONE_LIBRARY_ITEM, props: props }),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user