mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-25 12:30:37 +03:00
F: Improve node UI context menu
This commit is contained in:
parent
0d5d42a252
commit
0b8d66a172
1
TODO.txt
1
TODO.txt
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { ContextMenu } from './context-menu';
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 })
|
||||
}));
|
||||
|
|
Loading…
Reference in New Issue
Block a user