F: Improve node UI context menu

This commit is contained in:
Ivan 2025-04-22 22:10:41 +03:00
parent 3d81a7dc28
commit 6b68375b01
16 changed files with 447 additions and 312 deletions

View File

@ -15,6 +15,7 @@ User profile:
- Profile pictures
- Custom LibraryItem lists
- Custom user filters and sharing filters
- Personal prompt templates
- Static analyzer for RSForm as a whole: check term duplication and empty conventions
- OSS clone and versioning

View File

@ -0,0 +1,26 @@
'use client';
import { type IBlock } from '../models/oss';
interface InfoOperationProps {
block: IBlock;
}
export function InfoBlock({ block }: InfoOperationProps) {
return (
<>
{block.title ? (
<p>
<b>Название: </b>
{block.title}
</p>
) : null}
{block.description ? (
<p>
<b>Описание: </b>
{block.description}
</p>
) : null}
</>
);
}

View File

@ -1,21 +0,0 @@
import { Tooltip } from '@/components/container';
import { globalIDs } from '@/utils/constants';
import { useOperationTooltipStore } from '../stores/operation-tooltip';
import { InfoOperation } from './info-operation';
export function OperationTooltip() {
const hoverOperation = useOperationTooltipStore(state => state.activeOperation);
return (
<Tooltip
clickable
id={globalIDs.operation_tooltip}
layer='z-topmost'
className='max-w-140 dense max-h-120! overflow-y-auto!'
hidden={!hoverOperation}
>
{hoverOperation ? <InfoOperation operation={hoverOperation} /> : null}
</Tooltip>
);
}

View File

@ -0,0 +1,27 @@
import { Tooltip } from '@/components/container';
import { globalIDs } from '@/utils/constants';
import { type IBlock, type IOperation } from '../models/oss';
import { isOperation } from '../models/oss-api';
import { useOperationTooltipStore } from '../stores/operation-tooltip';
import { InfoBlock } from './info-block';
import { InfoOperation } from './info-operation';
export function OperationTooltip() {
const hoverItem = useOperationTooltipStore(state => state.hoverItem);
const isOperationNode = isOperation(hoverItem);
return (
<Tooltip
clickable
id={globalIDs.operation_tooltip}
layer='z-topmost'
className='max-w-140 dense max-h-120! overflow-y-auto!'
hidden={!hoverItem}
>
{hoverItem && isOperationNode ? <InfoOperation operation={hoverItem as IOperation} /> : null}
{hoverItem && !isOperationNode ? <InfoBlock block={hoverItem as IBlock} /> : null}
</Tooltip>
);
}

View File

@ -37,8 +37,8 @@ const DISTANCE_Y = 100; // pixels - insert y-distance between node centers
const STARTING_SUB_INDEX = 900; // max semantic index for starting substitution
/** Checks if element is {@link IOperation} or {@link IBlock}. */
export function isOperation(item: IOssItem): boolean {
return 'arguments' in item;
export function isOperation(item: IOssItem | null): boolean {
return !!item && 'arguments' in item;
}
/** Sorts library items relevant for the specified {@link IOperationSchema}. */
@ -528,7 +528,7 @@ export function calculateNewOperationPosition(
/** Calculate insert position for a new {@link IBlock} */
export function calculateNewBlockPosition(data: ICreateBlockDTO, layout: IOssLayout): Rectangle2D {
const block_nodes = data.children_blocks
.map(id => layout.blocks.find(block => block.id === -id))
.map(id => layout.blocks.find(block => block.id === id))
.filter(node => !!node);
const operation_nodes = data.children_operations
.map(id => layout.operations.find(operation => operation.id === id))
@ -544,25 +544,27 @@ export function calculateNewBlockPosition(data: ICreateBlockDTO, layout: IOssLay
let bottom = undefined;
for (const block of block_nodes) {
left = !left ? block.x - GRID_SIZE : Math.min(left, block.x - GRID_SIZE);
top = !top ? block.y - GRID_SIZE : Math.min(top, block.y - GRID_SIZE);
left = !left ? block.x - MIN_DISTANCE : Math.min(left, block.x - MIN_DISTANCE);
top = !top ? block.y - MIN_DISTANCE : Math.min(top, block.y - MIN_DISTANCE);
right = !right
? Math.max(left + data.width, block.x + block.width + GRID_SIZE)
: Math.max(right, block.x + block.width + GRID_SIZE);
? Math.max(left + data.width, block.x + block.width + MIN_DISTANCE)
: Math.max(right, block.x + block.width + MIN_DISTANCE);
bottom = !bottom
? Math.max(top + data.height, block.y + block.height + GRID_SIZE)
: Math.max(bottom, block.y + block.height + GRID_SIZE);
? Math.max(top + data.height, block.y + block.height + MIN_DISTANCE)
: Math.max(bottom, block.y + block.height + MIN_DISTANCE);
}
console.log('left, top, right, bottom', left, top, right, bottom);
for (const operation of operation_nodes) {
left = !left ? operation.x - GRID_SIZE : Math.min(left, operation.x - GRID_SIZE);
top = !top ? operation.y - GRID_SIZE : Math.min(top, operation.y - GRID_SIZE);
left = !left ? operation.x - MIN_DISTANCE : Math.min(left, operation.x - MIN_DISTANCE);
top = !top ? operation.y - MIN_DISTANCE : Math.min(top, operation.y - MIN_DISTANCE);
right = !right
? Math.max(left + data.width, operation.x + OPERATION_NODE_WIDTH + GRID_SIZE)
: Math.max(right, operation.x + OPERATION_NODE_WIDTH + GRID_SIZE);
? Math.max(left + data.width, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE)
: Math.max(right, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE);
bottom = !bottom
? Math.max(top + data.height, operation.y + OPERATION_NODE_HEIGHT + GRID_SIZE)
: Math.max(bottom, operation.y + OPERATION_NODE_HEIGHT + GRID_SIZE);
? Math.max(top + data.height, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE)
: Math.max(bottom, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE);
}
return {

View File

@ -22,7 +22,8 @@ export interface OssNode extends Node {
id: string;
data: {
label: string;
operation: IOperation;
operation?: IOperation;
block?: IBlock;
};
position: { x: number; y: number };
}

View File

@ -0,0 +1,62 @@
'use client';
import { useRef } from 'react';
import { isOperation } from '@/features/oss/models/oss-api';
import { Dropdown } from '@/components/dropdown';
import { type IBlock, type IOperation, type IOssItem } from '../../../../models/oss';
import { MenuBlock } from './menu-block';
import { MenuOperation } from './menu-operation';
// pixels - size of OSS context menu
const MENU_WIDTH = 200;
const MENU_HEIGHT = 200;
export interface ContextMenuData {
item: IOssItem | null;
cursorX: number;
cursorY: number;
}
interface ContextMenuProps extends ContextMenuData {
isOpen: boolean;
onHide: () => void;
}
export function ContextMenu({ isOpen, item, cursorX, cursorY, onHide }: ContextMenuProps) {
const ref = useRef<HTMLDivElement>(null);
const isOperationNode = isOperation(item);
function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
if (!ref.current?.contains(event.relatedTarget as Node)) {
onHide();
}
}
return (
<div
ref={ref}
onBlur={handleBlur}
className='relative'
style={{ top: `calc(${cursorY}px - 2.5rem)`, left: cursorX }}
>
<Dropdown
isOpen={isOpen}
stretchLeft={cursorX >= window.innerWidth - MENU_WIDTH}
stretchTop={cursorY >= window.innerHeight - MENU_HEIGHT}
margin={cursorY >= window.innerHeight - MENU_HEIGHT ? 'mb-3' : 'mt-3'}
>
{!!item ? (
isOperationNode ? (
<MenuOperation operation={item as IOperation} onHide={onHide} />
) : (
<MenuBlock block={item as IBlock} onHide={onHide} />
)
) : null}
</Dropdown>
</div>
);
}

View File

@ -0,0 +1 @@
export { ContextMenu } from './context-menu';

View File

@ -0,0 +1,61 @@
'use client';
import { useDeleteBlock } from '@/features/oss/backend/use-delete-block';
import { DropdownButton } from '@/components/dropdown';
import { IconDestroy, IconEdit2 } from '@/components/icons';
import { useDialogsStore } from '@/stores/dialogs';
import { useMutatingOss } from '../../../../backend/use-mutating-oss';
import { type IBlock } from '../../../../models/oss';
import { useOssEdit } from '../../oss-edit-context';
import { useGetLayout } from '../use-get-layout';
interface MenuBlockProps {
block: IBlock;
onHide: () => void;
}
export function MenuBlock({ block, onHide }: MenuBlockProps) {
const { schema, isMutable } = useOssEdit();
const isProcessing = useMutatingOss();
const getLayout = useGetLayout();
const showEditBlock = useDialogsStore(state => state.showEditBlock);
const { deleteBlock } = useDeleteBlock();
function handleEditOperation() {
if (!block) {
return;
}
onHide();
showEditBlock({
oss: schema,
target: block,
layout: getLayout()
});
}
function handleDeleteBlock() {
onHide();
void deleteBlock({ itemID: schema.id, data: { target: block.id, layout: getLayout() } });
}
return (
<>
<DropdownButton
text='Редактировать'
title='Редактировать блок'
icon={<IconEdit2 size='1rem' className='icon-primary' />}
onClick={handleEditOperation}
disabled={!isMutable || isProcessing}
/>
<DropdownButton
text='Удалить блок'
icon={<IconDestroy size='1rem' className='icon-red' />}
onClick={handleDeleteBlock}
disabled={!isMutable || isProcessing}
/>
</>
);
}

View File

@ -0,0 +1,224 @@
'use client';
import { toast } from 'react-toastify';
import { urls, useConceptNavigation } from '@/app';
import { useLibrary } from '@/features/library/backend/use-library';
import { useCreateInput } from '@/features/oss/backend/use-create-input';
import { useExecuteOperation } from '@/features/oss/backend/use-execute-operation';
import { DropdownButton } from '@/components/dropdown';
import {
IconChild,
IconConnect,
IconDestroy,
IconEdit2,
IconExecute,
IconNewRSForm,
IconRSForm
} from '@/components/icons';
import { useDialogsStore } from '@/stores/dialogs';
import { errorMsg } from '@/utils/labels';
import { prepareTooltip } from '@/utils/utils';
import { OperationType } from '../../../../backend/types';
import { useMutatingOss } from '../../../../backend/use-mutating-oss';
import { type IOperation } from '../../../../models/oss';
import { useOssEdit } from '../../oss-edit-context';
import { useGetLayout } from '../use-get-layout';
interface MenuOperationProps {
operation: IOperation;
onHide: () => void;
}
export function MenuOperation({ operation, onHide }: MenuOperationProps) {
const router = useConceptNavigation();
const { items: libraryItems } = useLibrary();
const { schema, navigateOperationSchema, isMutable, canDeleteOperation } = useOssEdit();
const isProcessing = useMutatingOss();
const getLayout = useGetLayout();
const { createInput: inputCreate } = useCreateInput();
const { executeOperation: operationExecute } = useExecuteOperation();
const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
const showEditOperation = useDialogsStore(state => state.showEditOperation);
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
const readyForSynthesis = (() => {
if (operation?.operation_type !== OperationType.SYNTHESIS) {
return false;
}
if (operation.result) {
return false;
}
const argumentIDs = schema.graph.expandInputs([operation.id]);
if (!argumentIDs || argumentIDs.length < 1) {
return false;
}
const argumentOperations = argumentIDs.map(id => schema.operationByID.get(id)!);
if (argumentOperations.some(item => item.result === null)) {
return false;
}
return true;
})();
function handleOpenSchema() {
if (!operation) {
return;
}
onHide();
navigateOperationSchema(operation.id);
}
function handleEditSchema() {
if (!operation) {
return;
}
onHide();
showEditInput({
oss: schema,
target: operation,
layout: getLayout()
});
}
function handleEditOperation() {
if (!operation) {
return;
}
onHide();
showEditOperation({
oss: schema,
target: operation,
layout: getLayout()
});
}
function handleDeleteOperation() {
if (!operation || !canDeleteOperation(operation)) {
return;
}
onHide();
showDeleteOperation({
oss: schema,
target: operation,
layout: getLayout()
});
}
function handleOperationExecute() {
if (!operation) {
return;
}
onHide();
void operationExecute({
itemID: schema.id, //
data: { target: operation.id, layout: getLayout() }
});
}
function handleInputCreate() {
if (!operation) {
return;
}
if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) {
toast.error(errorMsg.inputAlreadyExists);
return;
}
onHide();
void inputCreate({
itemID: schema.id,
data: { target: operation.id, layout: getLayout() }
}).then(new_schema => router.push({ path: urls.schema(new_schema.id), force: true }));
}
function handleRelocateConstituents() {
if (!operation) {
return;
}
onHide();
showRelocateConstituents({
oss: schema,
initialTarget: operation,
layout: getLayout()
});
}
return (
<>
<DropdownButton
text='Редактировать'
title='Редактировать операцию'
icon={<IconEdit2 size='1rem' className='icon-primary' />}
onClick={handleEditOperation}
disabled={!isMutable || isProcessing}
/>
{operation?.result ? (
<DropdownButton
text='Открыть схему'
titleHtml={prepareTooltip('Открыть привязанную КС', 'Двойной клик')}
aria-label='Открыть привязанную КС'
icon={<IconRSForm size='1rem' className='icon-green' />}
onClick={handleOpenSchema}
disabled={isProcessing}
/>
) : null}
{isMutable && !operation?.result && operation?.arguments.length === 0 ? (
<DropdownButton
text='Создать схему'
title='Создать пустую схему'
icon={<IconNewRSForm size='1rem' className='icon-green' />}
onClick={handleInputCreate}
disabled={isProcessing}
/>
) : null}
{isMutable && operation?.operation_type === OperationType.INPUT ? (
<DropdownButton
text={!operation?.result ? 'Загрузить схему' : 'Изменить схему'}
title='Выбрать схему для загрузки'
icon={<IconConnect size='1rem' className='icon-primary' />}
onClick={handleEditSchema}
disabled={isProcessing}
/>
) : null}
{isMutable && !operation?.result && operation?.operation_type === OperationType.SYNTHESIS ? (
<DropdownButton
text='Активировать синтез'
titleHtml={
readyForSynthesis
? 'Активировать операцию<br/>и получить синтезированную КС'
: 'Необходимо предоставить все аргументы'
}
aria-label='Активировать операцию и получить синтезированную КС'
icon={<IconExecute size='1rem' className='icon-green' />}
onClick={handleOperationExecute}
disabled={isProcessing || !readyForSynthesis}
/>
) : null}
{isMutable && operation?.result ? (
<DropdownButton
text='Конституенты'
titleHtml='Перенос конституент</br>между схемами'
aria-label='Перенос конституент между схемами'
icon={<IconChild size='1rem' className='icon-green' />}
onClick={handleRelocateConstituents}
disabled={isProcessing}
/>
) : null}
<DropdownButton
text='Удалить операцию'
icon={<IconDestroy size='1rem' className='icon-red' />}
onClick={handleDeleteOperation}
disabled={!isMutable || isProcessing || !operation || !canDeleteOperation(operation)}
/>
</>
);
}

View File

@ -3,9 +3,11 @@
import { NodeResizeControl } from 'reactflow';
import clsx from 'clsx';
import { useOperationTooltipStore } from '@/features/oss/stores/operation-tooltip';
import { useOSSGraphStore } from '@/features/oss/stores/oss-graph';
import { IconResize } from '@/components/icons';
import { globalIDs } from '@/utils/constants';
import { type BlockInternalNode } from '../../../../models/oss-layout';
import { useOssEdit } from '../../oss-edit-context';
@ -14,8 +16,10 @@ export const BLOCK_NODE_MIN_WIDTH = 160;
export const BLOCK_NODE_MIN_HEIGHT = 100;
export function BlockNode(node: BlockInternalNode) {
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
const { selected, schema } = useOssEdit();
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
const setHover = useOperationTooltipStore(state => state.setHoverItem);
const focus = selected.length === 1 ? selected[0] : null;
const isParent = (!!focus && schema.hierarchy.at(focus)?.inputs.includes(-node.data.block.id)) ?? false;
const isChild = (!!focus && schema.hierarchy.at(focus)?.outputs.includes(-node.data.block.id)) ?? false;
@ -50,6 +54,9 @@ export function BlockNode(node: BlockInternalNode) {
'text-[18px]/[20px] line-clamp-2 text-center text-ellipsis',
'pointer-events-auto'
)}
data-tooltip-id={globalIDs.operation_tooltip}
data-tooltip-hidden={node.dragging}
onMouseEnter={() => setHover(node.data.block)}
>
{node.data.label}
</div>

View File

@ -29,7 +29,7 @@ export function NodeCore({ node }: NodeCoreProps) {
const focus = selected.length === 1 ? selected[0] : null;
const isChild = (!!focus && schema.hierarchy.at(focus)?.outputs.includes(node.data.operation.id)) ?? false;
const setHover = useOperationTooltipStore(state => state.setActiveOperation);
const setHover = useOperationTooltipStore(state => state.setHoverItem);
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
const hasFile = !!node.data.operation.result;

View File

@ -1,257 +0,0 @@
'use client';
import { useRef } from 'react';
import { toast } from 'react-toastify';
import { urls, useConceptNavigation } from '@/app';
import { useLibrary } from '@/features/library/backend/use-library';
import { useCreateInput } from '@/features/oss/backend/use-create-input';
import { useExecuteOperation } from '@/features/oss/backend/use-execute-operation';
import { Dropdown, DropdownButton } from '@/components/dropdown';
import {
IconChild,
IconConnect,
IconDestroy,
IconEdit2,
IconExecute,
IconNewRSForm,
IconRSForm
} from '@/components/icons';
import { useDialogsStore } from '@/stores/dialogs';
import { errorMsg } from '@/utils/labels';
import { prepareTooltip } from '@/utils/utils';
import { OperationType } from '../../../backend/types';
import { useMutatingOss } from '../../../backend/use-mutating-oss';
import { type IOperation } from '../../../models/oss';
import { useOssEdit } from '../oss-edit-context';
import { useGetLayout } from './use-get-layout';
// pixels - size of OSS context menu
const MENU_WIDTH = 200;
const MENU_HEIGHT = 200;
export interface ContextMenuData {
operation: IOperation | null;
cursorX: number;
cursorY: number;
}
interface NodeContextMenuProps extends ContextMenuData {
isOpen: boolean;
onHide: () => void;
}
export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: NodeContextMenuProps) {
const router = useConceptNavigation();
const { items: libraryItems } = useLibrary();
const { schema, navigateOperationSchema, isMutable, canDeleteOperation: canDelete } = useOssEdit();
const isProcessing = useMutatingOss();
const getLayout = useGetLayout();
const { createInput: inputCreate } = useCreateInput();
const { executeOperation: operationExecute } = useExecuteOperation();
const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
const showEditOperation = useDialogsStore(state => state.showEditOperation);
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
const readyForSynthesis = (() => {
if (operation?.operation_type !== OperationType.SYNTHESIS) {
return false;
}
if (operation.result) {
return false;
}
const argumentIDs = schema.graph.expandInputs([operation.id]);
if (!argumentIDs || argumentIDs.length < 1) {
return false;
}
const argumentOperations = argumentIDs.map(id => schema.operationByID.get(id)!);
if (argumentOperations.some(item => item.result === null)) {
return false;
}
return true;
})();
const ref = useRef<HTMLDivElement>(null);
function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
if (!ref.current?.contains(event.relatedTarget as Node)) {
onHide();
}
}
function handleOpenSchema() {
if (!operation) {
return;
}
onHide();
navigateOperationSchema(operation.id);
}
function handleEditSchema() {
if (!operation) {
return;
}
onHide();
showEditInput({
oss: schema,
target: operation,
layout: getLayout()
});
}
function handleEditOperation() {
if (!operation) {
return;
}
onHide();
showEditOperation({
oss: schema,
target: operation,
layout: getLayout()
});
}
function handleDeleteOperation() {
if (!operation || !canDelete(operation)) {
return;
}
onHide();
showDeleteOperation({
oss: schema,
target: operation,
layout: getLayout()
});
}
function handleOperationExecute() {
if (!operation) {
return;
}
onHide();
void operationExecute({
itemID: schema.id, //
data: { target: operation.id, layout: getLayout() }
});
}
function handleInputCreate() {
if (!operation) {
return;
}
if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) {
toast.error(errorMsg.inputAlreadyExists);
return;
}
onHide();
void inputCreate({
itemID: schema.id,
data: { target: operation.id, layout: getLayout() }
}).then(new_schema => router.push({ path: urls.schema(new_schema.id), force: true }));
}
function handleRelocateConstituents() {
if (!operation) {
return;
}
onHide();
showRelocateConstituents({
oss: schema,
initialTarget: operation,
layout: getLayout()
});
}
return (
<div
ref={ref}
onBlur={handleBlur}
className='relative'
style={{ top: `calc(${cursorY}px - 2.5rem)`, left: cursorX }}
>
<Dropdown
isOpen={isOpen}
stretchLeft={cursorX >= window.innerWidth - MENU_WIDTH}
stretchTop={cursorY >= window.innerHeight - MENU_HEIGHT}
margin={cursorY >= window.innerHeight - MENU_HEIGHT ? 'mb-3' : 'mt-3'}
>
<DropdownButton
text='Редактировать'
title='Редактировать операцию'
icon={<IconEdit2 size='1rem' className='icon-primary' />}
onClick={handleEditOperation}
disabled={!isMutable || isProcessing}
/>
{operation?.result ? (
<DropdownButton
text='Открыть схему'
titleHtml={prepareTooltip('Открыть привязанную КС', 'Двойной клик')}
aria-label='Открыть привязанную КС'
icon={<IconRSForm size='1rem' className='icon-green' />}
onClick={handleOpenSchema}
disabled={isProcessing}
/>
) : null}
{isMutable && !operation?.result && operation?.arguments.length === 0 ? (
<DropdownButton
text='Создать схему'
title='Создать пустую схему'
icon={<IconNewRSForm size='1rem' className='icon-green' />}
onClick={handleInputCreate}
disabled={isProcessing}
/>
) : null}
{isMutable && operation?.operation_type === OperationType.INPUT ? (
<DropdownButton
text={!operation?.result ? 'Загрузить схему' : 'Изменить схему'}
title='Выбрать схему для загрузки'
icon={<IconConnect size='1rem' className='icon-primary' />}
onClick={handleEditSchema}
disabled={isProcessing}
/>
) : null}
{isMutable && !operation?.result && operation?.operation_type === OperationType.SYNTHESIS ? (
<DropdownButton
text='Активировать синтез'
titleHtml={
readyForSynthesis
? 'Активировать операцию<br/>и получить синтезированную КС'
: 'Необходимо предоставить все аргументы'
}
aria-label='Активировать операцию и получить синтезированную КС'
icon={<IconExecute size='1rem' className='icon-green' />}
onClick={handleOperationExecute}
disabled={isProcessing || !readyForSynthesis}
/>
) : null}
{isMutable && operation?.result ? (
<DropdownButton
text='Конституенты'
titleHtml='Перенос конституент</br>между схемами'
aria-label='Перенос конституент между схемами'
icon={<IconChild size='1rem' className='icon-green' />}
onClick={handleRelocateConstituents}
disabled={isProcessing}
/>
) : null}
<DropdownButton
text='Удалить операцию'
icon={<IconDestroy size='1rem' className='icon-red' />}
onClick={handleDeleteOperation}
disabled={!isMutable || isProcessing || !operation || !canDelete(operation)}
/>
</Dropdown>
</div>
);
}

View File

@ -28,8 +28,8 @@ import { useOperationTooltipStore } from '../../../stores/operation-tooltip';
import { useOSSGraphStore } from '../../../stores/oss-graph';
import { useOssEdit } from '../oss-edit-context';
import { ContextMenu, type ContextMenuData } from './context-menu/context-menu';
import { OssNodeTypes } from './graph/oss-node-types';
import { type ContextMenuData, NodeContextMenu } from './node-context-menu';
import { ToolbarOssGraph } from './toolbar-oss-graph';
import { useGetLayout } from './use-get-layout';
@ -53,11 +53,11 @@ export function OssFlow() {
} = useOssEdit();
const { fitView, screenToFlowPosition } = useReactFlow();
const store = useStoreApi();
const { resetSelectedElements } = store.getState();
const { resetSelectedElements, addSelectedNodes } = store.getState();
const isProcessing = useMutatingOss();
const setHoverOperation = useOperationTooltipStore(state => state.setActiveOperation);
const setHoverOperation = useOperationTooltipStore(state => state.setHoverItem);
const showGrid = useOSSGraphStore(state => state.showGrid);
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
@ -71,7 +71,7 @@ export function OssFlow() {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [toggleReset, setToggleReset] = useState(false);
const [menuProps, setMenuProps] = useState<ContextMenuData>({ operation: null, cursorX: 0, cursorY: 0 });
const [menuProps, setMenuProps] = useState<ContextMenuData>({ item: null, cursorX: 0, cursorY: 0 });
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
const [mouseCoords, setMouseCoords] = useState<Position2D>({ x: 0, y: 0 });
@ -202,12 +202,10 @@ export function OssFlow() {
event.preventDefault();
event.stopPropagation();
if (node.type === 'block') {
return;
}
addSelectedNodes([node.id]);
setMenuProps({
operation: node.data.operation,
item: node.type === 'block' ? node.data.block ?? null : node.data.operation ?? null,
cursorX: event.clientX,
cursorY: event.clientY
});
@ -216,9 +214,12 @@ export function OssFlow() {
}
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) {
if (node.type === 'block') {
return;
}
event.preventDefault();
event.stopPropagation();
if (node.data.operation.result) {
if (node.data.operation?.result) {
navigateOperationSchema(Number(node.id));
}
}
@ -285,7 +286,7 @@ export function OssFlow() {
onResetPositions={() => setToggleReset(prev => !prev)}
/>
<NodeContextMenu isOpen={isContextMenuOpen} onHide={() => setIsContextMenuOpen(false)} {...menuProps} />
<ContextMenu isOpen={isContextMenuOpen} onHide={() => setIsContextMenuOpen(false)} {...menuProps} />
<div className='cc-fade-in relative w-[100vw] cc-mask-sides' style={{ height: mainHeight, fontFamily: 'Rubik' }}>
<ReactFlow

View File

@ -14,7 +14,7 @@ import { type ErrorData } from '@/components/info-error';
import { useQueryStrings } from '@/hooks/use-query-strings';
import { useModificationStore } from '@/stores/modification';
import { OperationTooltip } from '../../components/operation-tooltip';
import { OperationTooltip } from '../../components/tooltip-oss-item';
import { OssTabID } from './oss-edit-context';
import { OssEditState } from './oss-edit-state';

View File

@ -1,13 +1,13 @@
import { create } from 'zustand';
import { type IOperation } from '../models/oss';
import { type IOssItem } from '../models/oss';
interface OperationTooltipStore {
activeOperation: IOperation | null;
setActiveOperation: (value: IOperation | null) => void;
hoverItem: IOssItem | null;
setHoverItem: (value: IOssItem | null) => void;
}
export const useOperationTooltipStore = create<OperationTooltipStore>()(set => ({
activeOperation: null,
setActiveOperation: value => set({ activeOperation: value })
hoverItem: null,
setHoverItem: value => set({ hoverItem: value })
}));