mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-25 20:40:36 +03:00
F: Improve diagram flow management
This commit is contained in:
parent
caf11aa329
commit
6bc7993797
100
rsconcept/frontend/src/components/flow/diagram-flow.tsx
Normal file
100
rsconcept/frontend/src/components/flow/diagram-flow.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { type ReactNode, useState } from 'react';
|
||||||
|
import { Background, ReactFlow, type ReactFlowProps } from 'reactflow';
|
||||||
|
|
||||||
|
export { useReactFlow, useStoreApi } from 'reactflow';
|
||||||
|
|
||||||
|
import { cn } from '../utils';
|
||||||
|
|
||||||
|
type DiagramFlowProps = {
|
||||||
|
children?: ReactNode;
|
||||||
|
height?: number | string;
|
||||||
|
showGrid?: boolean;
|
||||||
|
gridSize?: number;
|
||||||
|
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||||
|
onKeyUp?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||||
|
} & ReactFlowProps;
|
||||||
|
|
||||||
|
export function DiagramFlow({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
height,
|
||||||
|
showGrid = false,
|
||||||
|
gridSize = 20,
|
||||||
|
onKeyDown,
|
||||||
|
onKeyUp,
|
||||||
|
|
||||||
|
nodesDraggable = true,
|
||||||
|
nodesFocusable,
|
||||||
|
edgesFocusable,
|
||||||
|
|
||||||
|
onNodesChange,
|
||||||
|
onEdgesChange,
|
||||||
|
onNodeClick,
|
||||||
|
onNodeDoubleClick,
|
||||||
|
onNodeContextMenu,
|
||||||
|
onNodeMouseEnter,
|
||||||
|
onNodeMouseLeave,
|
||||||
|
|
||||||
|
onNodeDragStart,
|
||||||
|
onNodeDrag,
|
||||||
|
onNodeDragStop,
|
||||||
|
onContextMenu,
|
||||||
|
...restProps
|
||||||
|
}: DiagramFlowProps) {
|
||||||
|
const [spaceMode, setSpaceMode] = useState(false);
|
||||||
|
|
||||||
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
|
if (event.code === 'Space') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setSpaceMode(true);
|
||||||
|
}
|
||||||
|
onKeyDown?.(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyUp(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
|
if (event.code === 'Space') {
|
||||||
|
setSpaceMode(false);
|
||||||
|
}
|
||||||
|
onKeyUp?.(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContextMenu(event: React.MouseEvent<HTMLDivElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
onContextMenu?.(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
tabIndex={-1}
|
||||||
|
className={cn('relative cc-mask-sides w-[100dvw]', spaceMode && 'space-mode', className)}
|
||||||
|
style={{ ...style, height: height }}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
>
|
||||||
|
<ReactFlow
|
||||||
|
{...restProps}
|
||||||
|
onNodesChange={spaceMode ? undefined : onNodesChange}
|
||||||
|
onEdgesChange={spaceMode ? undefined : onEdgesChange}
|
||||||
|
onNodeClick={spaceMode ? undefined : onNodeClick}
|
||||||
|
onNodeDoubleClick={spaceMode ? undefined : onNodeDoubleClick}
|
||||||
|
nodesDraggable={!spaceMode && nodesDraggable}
|
||||||
|
nodesFocusable={!spaceMode && nodesFocusable}
|
||||||
|
edgesFocusable={!spaceMode && edgesFocusable}
|
||||||
|
onNodeDragStart={spaceMode ? undefined : onNodeDragStart}
|
||||||
|
onNodeDrag={spaceMode ? undefined : onNodeDrag}
|
||||||
|
onNodeDragStop={spaceMode ? undefined : onNodeDragStop}
|
||||||
|
onNodeContextMenu={spaceMode ? undefined : onNodeContextMenu}
|
||||||
|
onNodeMouseEnter={spaceMode ? undefined : onNodeMouseEnter}
|
||||||
|
onNodeMouseLeave={spaceMode ? undefined : onNodeMouseLeave}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
>
|
||||||
|
{showGrid ? <Background gap={gridSize} /> : null}
|
||||||
|
{children}
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -28,12 +28,12 @@ export function BlockNode(node: BlockInternalNode) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NodeResizeControl minWidth={BLOCK_NODE_MIN_WIDTH} minHeight={BLOCK_NODE_MIN_HEIGHT}>
|
<NodeResizeControl minWidth={BLOCK_NODE_MIN_WIDTH} minHeight={BLOCK_NODE_MIN_HEIGHT}>
|
||||||
<IconResize size={8} className='absolute bottom-[2px] right-[2px] cc-graph-interactive' />
|
<IconResize size={12} className='absolute bottom-[3px] right-[3px] cc-graph-interactive' />
|
||||||
</NodeResizeControl>
|
</NodeResizeControl>
|
||||||
{showCoordinates ? (
|
{showCoordinates ? (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute top-full mt-1 right-[1px]',
|
'absolute top-full mt-[4px] right-[1px]',
|
||||||
'text-[7px]/[8px] font-math',
|
'text-[7px]/[8px] font-math',
|
||||||
'text-muted-foreground hover:text-foreground'
|
'text-muted-foreground hover:text-foreground'
|
||||||
)}
|
)}
|
||||||
|
@ -43,24 +43,24 @@ export function BlockNode(node: BlockInternalNode) {
|
||||||
) : null}
|
) : null}
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'cc-node-block h-full w-full',
|
'cc-node-block h-full w-full cursor-pointer',
|
||||||
isDragging && isParent && dropTarget !== node.data.block.id && 'border-destructive',
|
isDragging && isParent && dropTarget !== node.data.block.id && 'border-destructive',
|
||||||
((isParent && !isDragging) || dropTarget === node.data.block.id) && 'border-primary',
|
((isParent && !isDragging) || dropTarget === node.data.block.id) && 'border-primary',
|
||||||
isChild && 'border-accent-orange'
|
isChild && 'border-accent-orange'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='absolute top-0 left-0 w-full h-2 cc-graph-interactive cursor-pointer' />
|
<div className='absolute -top-[8px] left-0 w-full h-[16px] cc-graph-interactive' />
|
||||||
<div className='absolute top-0 right-0 h-full w-2 cc-graph-interactive cursor-pointer' />
|
<div className='absolute top-0 -right-[8px] h-full w-[16px] cc-graph-interactive' />
|
||||||
<div className='absolute bottom-0 right-0 w-full h-2 cc-graph-interactive cursor-pointer' />
|
<div className='absolute -bottom-[8px] right-0 w-full h-[16px] cc-graph-interactive' />
|
||||||
<div className='absolute bottom-0 left-0 h-full w-2 cc-graph-interactive cursor-pointer' />
|
<div className='absolute bottom-0 -left-[8px] h-full w-[16px] cc-graph-interactive' />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-fit mx-auto -translate-y-1/2 -mt-[8px]',
|
'w-fit mx-auto -translate-y-1/2 -mt-[8px]',
|
||||||
'px-2',
|
'px-[8px]',
|
||||||
'bg-background rounded-lg',
|
'bg-background rounded-lg',
|
||||||
'text-[18px]/[20px] line-clamp-2 text-center text-ellipsis',
|
'text-[18px]/[20px] line-clamp-2 text-center text-ellipsis',
|
||||||
'cc-graph-interactive cursor-pointer'
|
'cc-graph-interactive'
|
||||||
)}
|
)}
|
||||||
data-tooltip-id={globalIDs.operation_tooltip}
|
data-tooltip-id={globalIDs.operation_tooltip}
|
||||||
data-tooltip-hidden={node.dragging}
|
data-tooltip-hidden={node.dragging}
|
||||||
|
|
|
@ -42,7 +42,7 @@ export function NodeCore({ node }: NodeCoreProps) {
|
||||||
data-tooltip-hidden={node.dragging}
|
data-tooltip-hidden={node.dragging}
|
||||||
onMouseEnter={() => setHover(node.data.operation)}
|
onMouseEnter={() => setHover(node.data.operation)}
|
||||||
>
|
>
|
||||||
<div className='absolute z-pop top-0 right-0 flex flex-col gap-1 p-[2px]'>
|
<div className='absolute z-pop top-0 right-0 flex flex-col gap-[4px] p-[2px]'>
|
||||||
<Indicator
|
<Indicator
|
||||||
noPadding
|
noPadding
|
||||||
title={hasFile ? 'Связанная КС' : 'Нет связанной КС'}
|
title={hasFile ? 'Связанная КС' : 'Нет связанной КС'}
|
||||||
|
@ -59,7 +59,7 @@ export function NodeCore({ node }: NodeCoreProps) {
|
||||||
{showCoordinates ? (
|
{showCoordinates ? (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute top-full mt-1 right-[1px]',
|
'absolute top-full mt-[4px] right-[1px]',
|
||||||
'text-[7px]/[8px] font-math',
|
'text-[7px]/[8px] font-math',
|
||||||
'text-muted-foreground hover:text-foreground',
|
'text-muted-foreground hover:text-foreground',
|
||||||
node.selected && 'translate-y-[6px]'
|
node.selected && 'translate-y-[6px]'
|
||||||
|
|
|
@ -11,10 +11,9 @@ import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import { useOssEdit } from '../oss-edit-context';
|
import { useOssEdit } from '../oss-edit-context';
|
||||||
|
|
||||||
|
import { flowOptions } from './oss-flow';
|
||||||
import { OssFlowContext } from './oss-flow-context';
|
import { OssFlowContext } from './oss-flow-context';
|
||||||
|
|
||||||
const VIEW_PADDING = 0.2;
|
|
||||||
|
|
||||||
const Z_BLOCK = 1;
|
const Z_BLOCK = 1;
|
||||||
const Z_SCHEMA = 10;
|
const Z_SCHEMA = 10;
|
||||||
|
|
||||||
|
@ -86,7 +85,7 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
||||||
setNodes(newNodes);
|
setNodes(newNodes);
|
||||||
setEdges(newEdges);
|
setEdges(newEdges);
|
||||||
|
|
||||||
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout);
|
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
|
||||||
}, [schema, setNodes, setEdges, edgeAnimate, edgeStraight, fitView]);
|
}, [schema, setNodes, setEdges, edgeAnimate, edgeStraight, fitView]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -94,7 +93,7 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
||||||
}, [schema, edgeAnimate, edgeStraight, resetGraph]);
|
}, [schema, edgeAnimate, edgeStraight, resetGraph]);
|
||||||
|
|
||||||
function resetView() {
|
function resetView() {
|
||||||
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout);
|
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Background, ReactFlow, useReactFlow, useStoreApi } from 'reactflow';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { DiagramFlow, useReactFlow, useStoreApi } from '@/components/flow/diagram-flow';
|
||||||
import { useMainHeight } from '@/stores/app-layout';
|
import { useMainHeight } from '@/stores/app-layout';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { promptText } from '@/utils/labels';
|
import { promptText } from '@/utils/labels';
|
||||||
|
|
||||||
import { useDeleteBlock } from '../../../backend/use-delete-block';
|
import { useDeleteBlock } from '../../../backend/use-delete-block';
|
||||||
|
@ -25,8 +26,18 @@ import { ToolbarOssGraph } from './toolbar-oss-graph';
|
||||||
import { useDragging } from './use-dragging';
|
import { useDragging } from './use-dragging';
|
||||||
import { useGetLayout } from './use-get-layout';
|
import { useGetLayout } from './use-get-layout';
|
||||||
|
|
||||||
const ZOOM_MAX = 2;
|
export const flowOptions = {
|
||||||
const ZOOM_MIN = 0.5;
|
fitView: true,
|
||||||
|
fitViewOptions: { padding: 0.3, duration: PARAMETER.zoomDuration },
|
||||||
|
edgesFocusable: false,
|
||||||
|
nodesFocusable: false,
|
||||||
|
nodesConnectable: false,
|
||||||
|
maxZoom: 2,
|
||||||
|
minZoom: 0.5,
|
||||||
|
gridSize: GRID_SIZE,
|
||||||
|
snapToGrid: true,
|
||||||
|
snapGrid: [GRID_SIZE, GRID_SIZE] as [number, number]
|
||||||
|
} as const;
|
||||||
|
|
||||||
export function OssFlow() {
|
export function OssFlow() {
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
|
@ -46,7 +57,6 @@ export function OssFlow() {
|
||||||
const { deleteBlock } = useDeleteBlock();
|
const { deleteBlock } = useDeleteBlock();
|
||||||
|
|
||||||
const [mouseCoords, setMouseCoords] = useState<Position2D>({ x: 0, y: 0 });
|
const [mouseCoords, setMouseCoords] = useState<Position2D>({ x: 0, y: 0 });
|
||||||
const [spacePressed, setSpacePressed] = useState(false);
|
|
||||||
|
|
||||||
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
||||||
const showCreateBlock = useDialogsStore(state => state.showCreateBlock);
|
const showCreateBlock = useDialogsStore(state => state.showCreateBlock);
|
||||||
|
@ -131,12 +141,6 @@ export function OssFlow() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (event.code === 'Space') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
setSpacePressed(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isProcessing) {
|
if (isProcessing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -173,25 +177,13 @@ export function OssFlow() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyUp(event: React.KeyboardEvent<HTMLDivElement>) {
|
|
||||||
if (event.code === 'Space') {
|
|
||||||
setSpacePressed(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseMove(event: React.MouseEvent<HTMLDivElement>) {
|
function handleMouseMove(event: React.MouseEvent<HTMLDivElement>) {
|
||||||
const targetPosition = screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
const targetPosition = screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
||||||
setMouseCoords(targetPosition);
|
setMouseCoords(targetPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div tabIndex={-1} className='relative' onMouseMove={showCoordinates ? handleMouseMove : undefined}>
|
||||||
tabIndex={-1}
|
|
||||||
className='relative'
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onKeyUp={handleKeyUp}
|
|
||||||
onMouseMove={showCoordinates ? handleMouseMove : undefined}
|
|
||||||
>
|
|
||||||
{showCoordinates ? <CoordinateDisplay mouseCoords={mouseCoords} className='absolute top-1 right-2' /> : null}
|
{showCoordinates ? <CoordinateDisplay mouseCoords={mouseCoords} className='absolute top-1 right-2' /> : null}
|
||||||
<ToolbarOssGraph
|
<ToolbarOssGraph
|
||||||
className='absolute z-pop top-8 right-1/2 translate-x-1/2'
|
className='absolute z-pop top-8 right-1/2 translate-x-1/2'
|
||||||
|
@ -203,43 +195,25 @@ export function OssFlow() {
|
||||||
|
|
||||||
<ContextMenu isOpen={isContextMenuOpen} onHide={hideContextMenu} {...menuProps} />
|
<ContextMenu isOpen={isContextMenuOpen} onHide={hideContextMenu} {...menuProps} />
|
||||||
|
|
||||||
<div
|
<DiagramFlow
|
||||||
className={clsx(
|
{...flowOptions}
|
||||||
'relative w-[100vw] cc-mask-sides',
|
className={clsx(!containMovement && 'cursor-relocate')}
|
||||||
spacePressed ? 'space-mode' : '',
|
height={mainHeight}
|
||||||
!containMovement && 'cursor-relocate'
|
onKeyDown={handleKeyDown}
|
||||||
)}
|
nodes={nodes}
|
||||||
style={{ height: mainHeight, fontFamily: 'Rubik' }}
|
edges={edges}
|
||||||
>
|
onNodesChange={onNodesChange}
|
||||||
<ReactFlow
|
onEdgesChange={onEdgesChange}
|
||||||
nodes={nodes}
|
nodeTypes={OssNodeTypes}
|
||||||
edges={edges}
|
showGrid={showGrid}
|
||||||
onNodesChange={spacePressed ? undefined : onNodesChange}
|
onClick={hideContextMenu}
|
||||||
onEdgesChange={spacePressed ? undefined : onEdgesChange}
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
edgesFocusable={false}
|
onNodeContextMenu={handleContextMenu}
|
||||||
nodesFocusable={false}
|
onContextMenu={hideContextMenu}
|
||||||
fitView
|
onNodeDragStart={handleDragStart}
|
||||||
nodeTypes={OssNodeTypes}
|
onNodeDrag={handleDrag}
|
||||||
maxZoom={ZOOM_MAX}
|
onNodeDragStop={handleDragStop}
|
||||||
minZoom={ZOOM_MIN}
|
/>
|
||||||
nodesConnectable={false}
|
|
||||||
snapToGrid={true}
|
|
||||||
snapGrid={[GRID_SIZE, GRID_SIZE]}
|
|
||||||
onClick={hideContextMenu}
|
|
||||||
onNodeDoubleClick={spacePressed ? undefined : handleNodeDoubleClick}
|
|
||||||
onNodeContextMenu={handleContextMenu}
|
|
||||||
onContextMenu={event => {
|
|
||||||
event.preventDefault();
|
|
||||||
hideContextMenu();
|
|
||||||
}}
|
|
||||||
nodesDraggable={!spacePressed}
|
|
||||||
onNodeDragStart={spacePressed ? undefined : handleDragStart}
|
|
||||||
onNodeDrag={spacePressed ? undefined : handleDrag}
|
|
||||||
onNodeDragStop={spacePressed ? undefined : handleDragStop}
|
|
||||||
>
|
|
||||||
{showGrid ? <Background gap={GRID_SIZE} /> : null}
|
|
||||||
</ReactFlow>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,9 @@ export function useDragging({ hideContextMenu }: DraggingProps) {
|
||||||
if (containMovement) {
|
if (containMovement) {
|
||||||
applyContainMovement([target.id, ...selected.map(id => String(id))], false);
|
applyContainMovement([target.id, ...selected.map(id => String(id))], false);
|
||||||
} else {
|
} else {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
const new_parent = dropTarget.evaluate(event);
|
const new_parent = dropTarget.evaluate(event);
|
||||||
const allSelected = [...selected.filter(id => id != Number(target.id)), Number(target.id)];
|
const allSelected = [...selected.filter(id => id != Number(target.id)), Number(target.id)];
|
||||||
const operations = allSelected
|
const operations = allSelected
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { type Edge, MarkerType, type Node, ReactFlow, useEdgesState, useNodesState } from 'reactflow';
|
import { type Edge, MarkerType, type Node, useEdgesState, useNodesState } from 'reactflow';
|
||||||
|
|
||||||
|
import { DiagramFlow } from '@/components/flow/diagram-flow';
|
||||||
|
|
||||||
import { type SyntaxTree } from '../../models/rslang';
|
import { type SyntaxTree } from '../../models/rslang';
|
||||||
|
|
||||||
|
@ -9,6 +11,16 @@ import { ASTEdgeTypes } from './graph/ast-edge-types';
|
||||||
import { applyLayout } from './graph/ast-layout';
|
import { applyLayout } from './graph/ast-layout';
|
||||||
import { ASTNodeTypes } from './graph/ast-node-types';
|
import { ASTNodeTypes } from './graph/ast-node-types';
|
||||||
|
|
||||||
|
const flowOptions = {
|
||||||
|
fitView: true,
|
||||||
|
fitViewOptions: { padding: 0.25 },
|
||||||
|
edgesFocusable: false,
|
||||||
|
nodesFocusable: false,
|
||||||
|
nodesConnectable: false,
|
||||||
|
maxZoom: 2,
|
||||||
|
minZoom: 0.5
|
||||||
|
} as const;
|
||||||
|
|
||||||
interface ASTFlowProps {
|
interface ASTFlowProps {
|
||||||
data: SyntaxTree;
|
data: SyntaxTree;
|
||||||
onNodeEnter: (node: Node) => void;
|
onNodeEnter: (node: Node) => void;
|
||||||
|
@ -53,11 +65,11 @@ export function ASTFlow({ data, onNodeEnter, onNodeLeave, onChangeDragging }: AS
|
||||||
}, [data, setNodes, setEdges]);
|
}, [data, setNodes, setEdges]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<DiagramFlow
|
||||||
|
{...flowOptions}
|
||||||
|
className='h-full w-full'
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
edgesFocusable={false}
|
|
||||||
nodesFocusable={false}
|
|
||||||
onNodeMouseEnter={(_, node) => onNodeEnter(node)}
|
onNodeMouseEnter={(_, node) => onNodeEnter(node)}
|
||||||
onNodeMouseLeave={(_, node) => onNodeLeave(node)}
|
onNodeMouseLeave={(_, node) => onNodeLeave(node)}
|
||||||
onNodeDragStart={() => onChangeDragging(true)}
|
onNodeDragStart={() => onChangeDragging(true)}
|
||||||
|
@ -65,11 +77,6 @@ export function ASTFlow({ data, onNodeEnter, onNodeLeave, onChangeDragging }: AS
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
nodeTypes={ASTNodeTypes}
|
nodeTypes={ASTNodeTypes}
|
||||||
edgeTypes={ASTEdgeTypes}
|
edgeTypes={ASTEdgeTypes}
|
||||||
fitView
|
|
||||||
maxZoom={2}
|
|
||||||
minZoom={0.5}
|
|
||||||
nodesConnectable={false}
|
|
||||||
onContextMenu={event => event.preventDefault()}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,9 @@ export function ASTNode(node: ASTNodeInternal) {
|
||||||
<Handle type='source' position={Position.Bottom} className='opacity-0' />
|
<Handle type='source' position={Position.Bottom} className='opacity-0' />
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'font-math mt-1 w-fit text-center translate-x-[calc(-50%+20px)]',
|
'mt-[4px] w-fit translate-x-[calc(-50%+20px)]',
|
||||||
|
'font-math text-center ',
|
||||||
|
'pointer-events-none',
|
||||||
label.length > LABEL_THRESHOLD ? 'text-[12px]/[16px]' : 'text-[14px]/[20px]'
|
label.length > LABEL_THRESHOLD ? 'text-[12px]/[16px]' : 'text-[14px]/[20px]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { type Edge, ReactFlow, useEdgesState, useNodesState } from 'reactflow';
|
import { type Edge, useEdgesState, useNodesState } from 'reactflow';
|
||||||
|
|
||||||
|
import { DiagramFlow } from '@/components/flow/diagram-flow';
|
||||||
|
|
||||||
import { type TypificationGraph } from '../../models/typification-graph';
|
import { type TypificationGraph } from '../../models/typification-graph';
|
||||||
|
|
||||||
|
@ -9,8 +11,15 @@ import { TMGraphEdgeTypes } from './graph/mgraph-edge-types';
|
||||||
import { applyLayout } from './graph/mgraph-layout';
|
import { applyLayout } from './graph/mgraph-layout';
|
||||||
import { TMGraphNodeTypes } from './graph/mgraph-node-types';
|
import { TMGraphNodeTypes } from './graph/mgraph-node-types';
|
||||||
|
|
||||||
const ZOOM_MAX = 2;
|
const flowOptions = {
|
||||||
const ZOOM_MIN = 0.5;
|
fitView: true,
|
||||||
|
fitViewOptions: { padding: 0.25 },
|
||||||
|
edgesFocusable: false,
|
||||||
|
nodesFocusable: false,
|
||||||
|
nodesConnectable: false,
|
||||||
|
maxZoom: 2,
|
||||||
|
minZoom: 0.5
|
||||||
|
} as const;
|
||||||
|
|
||||||
interface MGraphFlowProps {
|
interface MGraphFlowProps {
|
||||||
data: TypificationGraph;
|
data: TypificationGraph;
|
||||||
|
@ -57,19 +66,14 @@ export function MGraphFlow({ data }: MGraphFlowProps) {
|
||||||
}, [data, setNodes, setEdges]);
|
}, [data, setNodes, setEdges]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<DiagramFlow
|
||||||
|
{...flowOptions}
|
||||||
|
className='h-full w-full'
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
edgesFocusable={false}
|
|
||||||
nodesFocusable={false}
|
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
nodeTypes={TMGraphNodeTypes}
|
nodeTypes={TMGraphNodeTypes}
|
||||||
edgeTypes={TMGraphEdgeTypes}
|
edgeTypes={TMGraphEdgeTypes}
|
||||||
fitView
|
|
||||||
maxZoom={ZOOM_MAX}
|
|
||||||
minZoom={ZOOM_MIN}
|
|
||||||
nodesConnectable={false}
|
|
||||||
onContextMenu={event => event.preventDefault()}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ 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-2 border-selected',
|
isFocused && '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={{
|
||||||
|
@ -76,13 +76,14 @@ export function TGNode(node: TGNodeInternal) {
|
||||||
{description ? (
|
{description ? (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mt-1 w-[150px] px-1 text-center translate-x-[calc(-50%+20px)]',
|
'mt-[4px] w-[150px] px-[4px] text-center translate-x-[calc(-50%+20px)]',
|
||||||
|
'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}
|
onContextMenu={handleContextMenu}
|
||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleDoubleClick}
|
||||||
>
|
>
|
||||||
<div className='absolute top-0 px-1 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}
|
||||||
</div>
|
</div>
|
||||||
<div aria-hidden className='line-clamp-3 hover:line-clamp-none cc-text-outline'>
|
<div aria-hidden className='line-clamp-3 hover:line-clamp-none cc-text-outline'>
|
||||||
|
|
|
@ -1,18 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import {
|
import { type Edge, MarkerType, type Node, useEdgesState, useNodesState, useOnSelectionChange } from 'reactflow';
|
||||||
type Edge,
|
|
||||||
MarkerType,
|
|
||||||
type Node,
|
|
||||||
ReactFlow,
|
|
||||||
useEdgesState,
|
|
||||||
useNodesState,
|
|
||||||
useOnSelectionChange,
|
|
||||||
useReactFlow,
|
|
||||||
useStoreApi
|
|
||||||
} from 'reactflow';
|
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
@ -32,9 +23,15 @@ import { ToolbarTermGraph } from './toolbar-term-graph';
|
||||||
import { useFilteredGraph } from './use-filtered-graph';
|
import { useFilteredGraph } from './use-filtered-graph';
|
||||||
import { ViewHidden } from './view-hidden';
|
import { ViewHidden } from './view-hidden';
|
||||||
|
|
||||||
const ZOOM_MAX = 3;
|
export const flowOptions = {
|
||||||
const ZOOM_MIN = 0.25;
|
fitView: true,
|
||||||
export const VIEW_PADDING = 0.3;
|
fitViewOptions: { padding: 0.3, duration: PARAMETER.zoomDuration },
|
||||||
|
edgesFocusable: false,
|
||||||
|
nodesFocusable: false,
|
||||||
|
nodesConnectable: false,
|
||||||
|
maxZoom: 3,
|
||||||
|
minZoom: 0.25
|
||||||
|
} as const;
|
||||||
|
|
||||||
export function TGFlow() {
|
export function TGFlow() {
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
|
@ -116,9 +113,7 @@ export function TGFlow() {
|
||||||
setNodes(newNodes);
|
setNodes(newNodes);
|
||||||
setEdges(newEdges);
|
setEdges(newEdges);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.minimalTimeout);
|
||||||
fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING });
|
|
||||||
}, PARAMETER.minimalTimeout);
|
|
||||||
}, [schema, filteredGraph, setNodes, setEdges, filter.noText, fitView, viewportInitialized, focusCst]);
|
}, [schema, filteredGraph, setNodes, setEdges, filter.noText, fitView, viewportInitialized, focusCst]);
|
||||||
|
|
||||||
const prevSelected = useRef<number[]>([]);
|
const prevSelected = useRef<number[]>([]);
|
||||||
|
@ -190,22 +185,16 @@ export function TGFlow() {
|
||||||
<ViewHidden items={hidden} />
|
<ViewHidden items={hidden} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='relative outline-hidden w-[100dvw] cc-mask-sides' style={{ height: mainHeight }}>
|
<DiagramFlow
|
||||||
<ReactFlow
|
{...flowOptions}
|
||||||
nodes={nodes}
|
height={mainHeight}
|
||||||
onNodesChange={onNodesChange}
|
nodes={nodes}
|
||||||
edges={edges}
|
onNodesChange={onNodesChange}
|
||||||
fitView
|
edges={edges}
|
||||||
edgesFocusable={false}
|
nodeTypes={TGNodeTypes}
|
||||||
nodesFocusable={false}
|
edgeTypes={TGEdgeTypes}
|
||||||
nodesConnectable={false}
|
onContextMenu={event => event.preventDefault()}
|
||||||
nodeTypes={TGNodeTypes}
|
/>
|
||||||
edgeTypes={TGEdgeTypes}
|
|
||||||
maxZoom={ZOOM_MAX}
|
|
||||||
minZoom={ZOOM_MIN}
|
|
||||||
onContextMenu={event => event.preventDefault()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
|
||||||
import { useTermGraphStore } from '../../../stores/term-graph';
|
import { useTermGraphStore } from '../../../stores/term-graph';
|
||||||
import { useRSEdit } from '../rsedit-context';
|
import { useRSEdit } from '../rsedit-context';
|
||||||
|
|
||||||
import { VIEW_PADDING } from './tg-flow';
|
import { flowOptions } from './tg-flow';
|
||||||
|
|
||||||
export function ToolbarTermGraph() {
|
export function ToolbarTermGraph() {
|
||||||
const isProcessing = useMutatingRSForm();
|
const isProcessing = useMutatingRSForm();
|
||||||
|
@ -76,7 +76,7 @@ export function ToolbarTermGraph() {
|
||||||
|
|
||||||
function handleFitView() {
|
function handleFitView() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING });
|
fitView(flowOptions.fitViewOptions);
|
||||||
}, PARAMETER.minimalTimeout);
|
}, PARAMETER.minimalTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user