F: Improve OSS graph interactions
This commit is contained in:
parent
ba11c1f82b
commit
cab9ae8efc
|
@ -1,6 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
import { useLibrary } from '@/features/library/backend/useLibrary';
|
||||||
|
import { useInputCreate } from '@/features/oss/backend/useInputCreate';
|
||||||
|
import { useOperationExecute } from '@/features/oss/backend/useOperationExecute';
|
||||||
|
|
||||||
import { Dropdown, DropdownButton } from '@/components/Dropdown';
|
import { Dropdown, DropdownButton } from '@/components/Dropdown';
|
||||||
import {
|
import {
|
||||||
|
@ -13,6 +19,8 @@ import {
|
||||||
IconRSForm
|
IconRSForm
|
||||||
} from '@/components/Icons';
|
} from '@/components/Icons';
|
||||||
import { useClickedOutside } from '@/hooks/useClickedOutside';
|
import { useClickedOutside } from '@/hooks/useClickedOutside';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { errorMsg } from '@/utils/labels';
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
import { OperationType } from '../../../backend/types';
|
import { OperationType } from '../../../backend/types';
|
||||||
|
@ -20,12 +28,14 @@ import { useMutatingOss } from '../../../backend/useMutatingOss';
|
||||||
import { type IOperation } from '../../../models/oss';
|
import { type IOperation } from '../../../models/oss';
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
|
|
||||||
|
import { useGetPositions } from './useGetPositions';
|
||||||
|
|
||||||
// pixels - size of OSS context menu
|
// pixels - size of OSS context menu
|
||||||
const MENU_WIDTH = 200;
|
const MENU_WIDTH = 200;
|
||||||
const MENU_HEIGHT = 200;
|
const MENU_HEIGHT = 200;
|
||||||
|
|
||||||
export interface ContextMenuData {
|
export interface ContextMenuData {
|
||||||
operation: IOperation;
|
operation: IOperation | null;
|
||||||
cursorX: number;
|
cursorX: number;
|
||||||
cursorY: number;
|
cursorY: number;
|
||||||
}
|
}
|
||||||
|
@ -33,33 +43,25 @@ export interface ContextMenuData {
|
||||||
interface NodeContextMenuProps extends ContextMenuData {
|
interface NodeContextMenuProps extends ContextMenuData {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onHide: () => void;
|
onHide: () => void;
|
||||||
onDelete: (target: number) => void;
|
|
||||||
onCreateInput: (target: number) => void;
|
|
||||||
onEditSchema: (target: number) => void;
|
|
||||||
onEditOperation: (target: number) => void;
|
|
||||||
onExecuteOperation: (target: number) => void;
|
|
||||||
onRelocateConstituents: (target: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NodeContextMenu({
|
export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: NodeContextMenuProps) {
|
||||||
isOpen,
|
const router = useConceptNavigation();
|
||||||
operation,
|
const { items: libraryItems } = useLibrary();
|
||||||
cursorX,
|
const { schema, navigateOperationSchema, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||||
cursorY,
|
|
||||||
onHide,
|
|
||||||
onDelete,
|
|
||||||
onCreateInput,
|
|
||||||
onEditSchema,
|
|
||||||
onEditOperation,
|
|
||||||
onExecuteOperation,
|
|
||||||
onRelocateConstituents
|
|
||||||
}: NodeContextMenuProps) {
|
|
||||||
const isProcessing = useMutatingOss();
|
const isProcessing = useMutatingOss();
|
||||||
const { schema, navigateOperationSchema, isMutable, canDelete } = useOssEdit();
|
const getPositions = useGetPositions();
|
||||||
|
|
||||||
|
const { inputCreate } = useInputCreate();
|
||||||
|
const { operationExecute } = useOperationExecute();
|
||||||
|
|
||||||
|
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 ref = useRef<HTMLDivElement>(null);
|
|
||||||
const readyForSynthesis = (() => {
|
const readyForSynthesis = (() => {
|
||||||
if (operation.operation_type !== OperationType.SYNTHESIS) {
|
if (operation?.operation_type !== OperationType.SYNTHESIS) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (operation.result) {
|
if (operation.result) {
|
||||||
|
@ -79,40 +81,89 @@ export function NodeContextMenu({
|
||||||
return true;
|
return true;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
useClickedOutside(isOpen, ref, onHide);
|
useClickedOutside(isOpen, ref, onHide);
|
||||||
|
|
||||||
function handleOpenSchema() {
|
function handleOpenSchema() {
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onHide();
|
||||||
navigateOperationSchema(operation.id);
|
navigateOperationSchema(operation.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditSchema() {
|
function handleEditSchema() {
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onHide();
|
onHide();
|
||||||
onEditSchema(operation.id);
|
showEditInput({
|
||||||
|
oss: schema,
|
||||||
|
target: operation,
|
||||||
|
positions: getPositions()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditOperation() {
|
function handleEditOperation() {
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onHide();
|
onHide();
|
||||||
onEditOperation(operation.id);
|
showEditOperation({
|
||||||
|
oss: schema,
|
||||||
|
target: operation,
|
||||||
|
positions: getPositions()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteOperation() {
|
function handleDeleteOperation() {
|
||||||
|
if (!operation || !canDelete(operation)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onHide();
|
onHide();
|
||||||
onDelete(operation.id);
|
showDeleteOperation({
|
||||||
|
oss: schema,
|
||||||
|
target: operation,
|
||||||
|
positions: getPositions()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateSchema() {
|
function handleOperationExecute() {
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onHide();
|
onHide();
|
||||||
onCreateInput(operation.id);
|
void operationExecute({
|
||||||
|
itemID: schema.id, //
|
||||||
|
data: { target: operation.id, positions: getPositions() }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRunSynthesis() {
|
function handleInputCreate() {
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) {
|
||||||
|
toast.error(errorMsg.inputAlreadyExists);
|
||||||
|
return;
|
||||||
|
}
|
||||||
onHide();
|
onHide();
|
||||||
onExecuteOperation(operation.id);
|
void inputCreate({
|
||||||
|
itemID: schema.id,
|
||||||
|
data: { target: operation.id, positions: getPositions() }
|
||||||
|
}).then(new_schema => router.push(urls.schema(new_schema.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRelocateConstituents() {
|
function handleRelocateConstituents() {
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onHide();
|
onHide();
|
||||||
onRelocateConstituents(operation.id);
|
showRelocateConstituents({
|
||||||
|
oss: schema,
|
||||||
|
initialTarget: operation,
|
||||||
|
positions: getPositions()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -145,7 +196,7 @@ export function NodeContextMenu({
|
||||||
title='Создать пустую схему для загрузки'
|
title='Создать пустую схему для загрузки'
|
||||||
icon={<IconNewRSForm size='1rem' className='icon-green' />}
|
icon={<IconNewRSForm size='1rem' className='icon-green' />}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={handleCreateSchema}
|
onClick={handleInputCreate}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{isMutable && operation?.operation_type === OperationType.INPUT ? (
|
{isMutable && operation?.operation_type === OperationType.INPUT ? (
|
||||||
|
@ -167,7 +218,7 @@ export function NodeContextMenu({
|
||||||
}
|
}
|
||||||
icon={<IconExecute size='1rem' className='icon-green' />}
|
icon={<IconExecute size='1rem' className='icon-green' />}
|
||||||
disabled={isProcessing || !readyForSynthesis}
|
disabled={isProcessing || !readyForSynthesis}
|
||||||
onClick={handleRunSynthesis}
|
onClick={handleOperationExecute}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
@ -184,7 +235,7 @@ export function NodeContextMenu({
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Удалить операцию'
|
text='Удалить операцию'
|
||||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||||
disabled={!isMutable || isProcessing || !operation || !canDelete(operation.id)}
|
disabled={!isMutable || isProcessing || !operation || !canDelete(operation)}
|
||||||
onClick={handleDeleteOperation}
|
onClick={handleDeleteOperation}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import {
|
import {
|
||||||
Background,
|
Background,
|
||||||
type Node,
|
type Node,
|
||||||
|
@ -12,18 +11,13 @@ import {
|
||||||
useReactFlow
|
useReactFlow
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
|
||||||
import { useLibrary } from '@/features/library/backend/useLibrary';
|
|
||||||
|
|
||||||
import { Overlay } from '@/components/Container';
|
import { Overlay } from '@/components/Container';
|
||||||
import { useMainHeight } from '@/stores/appLayout';
|
import { useMainHeight } from '@/stores/appLayout';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { useTooltipsStore } from '@/stores/tooltips';
|
import { useTooltipsStore } from '@/stores/tooltips';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { errorMsg } from '@/utils/labels';
|
|
||||||
|
|
||||||
import { useInputCreate } from '../../../backend/useInputCreate';
|
|
||||||
import { useMutatingOss } from '../../../backend/useMutatingOss';
|
import { useMutatingOss } from '../../../backend/useMutatingOss';
|
||||||
import { useOperationExecute } from '../../../backend/useOperationExecute';
|
|
||||||
import { useUpdatePositions } from '../../../backend/useUpdatePositions';
|
import { useUpdatePositions } from '../../../backend/useUpdatePositions';
|
||||||
import { GRID_SIZE } from '../../../models/ossAPI';
|
import { GRID_SIZE } from '../../../models/ossAPI';
|
||||||
import { type OssNode } from '../../../models/ossLayout';
|
import { type OssNode } from '../../../models/ossLayout';
|
||||||
|
@ -33,9 +27,11 @@ import { useOssEdit } from '../OssEditContext';
|
||||||
import { OssNodeTypes } from './graph/OssNodeTypes';
|
import { OssNodeTypes } from './graph/OssNodeTypes';
|
||||||
import { type ContextMenuData, NodeContextMenu } from './NodeContextMenu';
|
import { type ContextMenuData, NodeContextMenu } from './NodeContextMenu';
|
||||||
import { ToolbarOssGraph } from './ToolbarOssGraph';
|
import { ToolbarOssGraph } from './ToolbarOssGraph';
|
||||||
|
import { useGetPositions } from './useGetPositions';
|
||||||
|
|
||||||
const ZOOM_MAX = 2;
|
const ZOOM_MAX = 2;
|
||||||
const ZOOM_MIN = 0.5;
|
const ZOOM_MIN = 0.5;
|
||||||
|
export const VIEW_PADDING = 0.2;
|
||||||
|
|
||||||
export function OssFlow() {
|
export function OssFlow() {
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
|
@ -45,16 +41,9 @@ export function OssFlow() {
|
||||||
setSelected,
|
setSelected,
|
||||||
selected,
|
selected,
|
||||||
isMutable,
|
isMutable,
|
||||||
promptCreateOperation,
|
canDeleteOperation: canDelete
|
||||||
canDelete,
|
|
||||||
promptDeleteOperation,
|
|
||||||
promptEditInput,
|
|
||||||
promptEditOperation,
|
|
||||||
promptRelocateConstituents
|
|
||||||
} = useOssEdit();
|
} = useOssEdit();
|
||||||
const router = useConceptNavigation();
|
const { fitView, project } = useReactFlow();
|
||||||
const { items: libraryItems } = useLibrary();
|
|
||||||
const flow = useReactFlow();
|
|
||||||
|
|
||||||
const isProcessing = useMutatingOss();
|
const isProcessing = useMutatingOss();
|
||||||
|
|
||||||
|
@ -64,16 +53,18 @@ export function OssFlow() {
|
||||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||||
|
|
||||||
const { inputCreate } = useInputCreate();
|
const getPositions = useGetPositions();
|
||||||
const { operationExecute } = useOperationExecute();
|
|
||||||
const { updatePositions } = useUpdatePositions();
|
const { updatePositions } = useUpdatePositions();
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
const [toggleReset, setToggleReset] = useState(false);
|
const [toggleReset, setToggleReset] = useState(false);
|
||||||
const [menuProps, setMenuProps] = useState<ContextMenuData | null>(null);
|
const [menuProps, setMenuProps] = useState<ContextMenuData>({ operation: null, cursorX: 0, cursorY: 0 });
|
||||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
||||||
|
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||||
|
|
||||||
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
||||||
const ids = nodes.map(node => Number(node.id));
|
const ids = nodes.map(node => Number(node.id));
|
||||||
setSelected(prev => [
|
setSelected(prev => [
|
||||||
|
@ -109,15 +100,8 @@ export function OssFlow() {
|
||||||
: 'left'
|
: 'left'
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate]);
|
fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING });
|
||||||
|
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]);
|
||||||
function getPositions() {
|
|
||||||
return nodes.map(node => ({
|
|
||||||
id: Number(node.id),
|
|
||||||
position_x: node.position.x,
|
|
||||||
position_y: node.position.y
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSavePositions() {
|
function handleSavePositions() {
|
||||||
const positions = getPositions();
|
const positions = getPositions();
|
||||||
|
@ -132,73 +116,34 @@ export function OssFlow() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateOperation(inputs: number[]) {
|
function handleCreateOperation() {
|
||||||
const positions = getPositions();
|
const targetPosition = project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||||
const target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
showCreateOperation({
|
||||||
promptCreateOperation({
|
oss: schema,
|
||||||
defaultX: target.x,
|
defaultX: targetPosition.x,
|
||||||
defaultY: target.y,
|
defaultY: targetPosition.y,
|
||||||
inputs: inputs,
|
positions: getPositions(),
|
||||||
positions: positions,
|
initialInputs: selected,
|
||||||
callback: () => setTimeout(() => flow.fitView({ duration: PARAMETER.zoomDuration }), PARAMETER.refreshTimeout)
|
onCreate: () =>
|
||||||
|
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteOperation(target: number) {
|
|
||||||
if (!canDelete(target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
promptDeleteOperation(target, getPositions());
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteSelected() {
|
function handleDeleteSelected() {
|
||||||
if (selected.length !== 1) {
|
if (selected.length !== 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleDeleteOperation(selected[0]);
|
const operation = schema.operationByID.get(selected[0]);
|
||||||
}
|
if (!operation || !canDelete(operation)) {
|
||||||
|
|
||||||
function handleInputCreate(target: number) {
|
|
||||||
const operation = schema.operationByID.get(target);
|
|
||||||
if (!operation) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) {
|
showDeleteOperation({
|
||||||
toast.error(errorMsg.inputAlreadyExists);
|
oss: schema,
|
||||||
return;
|
target: operation,
|
||||||
}
|
positions: getPositions()
|
||||||
void inputCreate({
|
|
||||||
itemID: schema.id,
|
|
||||||
data: { target: target, positions: getPositions() }
|
|
||||||
}).then(new_schema => router.push(urls.schema(new_schema.id)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEditSchema(target: number) {
|
|
||||||
promptEditInput(target, getPositions());
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEditOperation(target: number) {
|
|
||||||
promptEditOperation(target, getPositions());
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOperationExecute(target: number) {
|
|
||||||
void operationExecute({
|
|
||||||
itemID: schema.id, //
|
|
||||||
data: { target: target, positions: getPositions() }
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExecuteSelected() {
|
|
||||||
if (selected.length !== 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleOperationExecute(selected[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRelocateConstituents(target: number) {
|
|
||||||
promptRelocateConstituents(target, getPositions());
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
|
function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -212,14 +157,6 @@ export function OssFlow() {
|
||||||
setHoverOperation(null);
|
setHoverOperation(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContextMenuHide() {
|
|
||||||
setIsContextMenuOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCanvasClick() {
|
|
||||||
handleContextMenuHide();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) {
|
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -229,10 +166,7 @@ export function OssFlow() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (isProcessing) {
|
if (isProcessing || !isMutable) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isMutable) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
||||||
|
@ -244,7 +178,7 @@ export function OssFlow() {
|
||||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') {
|
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleCreateOperation(selected);
|
handleCreateOperation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === 'Delete') {
|
if (event.key === 'Delete') {
|
||||||
|
@ -262,35 +196,20 @@ export function OssFlow() {
|
||||||
className='rounded-b-2xl cc-blur hover:bg-prim-100 hover:bg-opacity-50'
|
className='rounded-b-2xl cc-blur hover:bg-prim-100 hover:bg-opacity-50'
|
||||||
>
|
>
|
||||||
<ToolbarOssGraph
|
<ToolbarOssGraph
|
||||||
onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })}
|
onCreate={handleCreateOperation}
|
||||||
onCreate={() => handleCreateOperation(selected)}
|
|
||||||
onDelete={handleDeleteSelected}
|
onDelete={handleDeleteSelected}
|
||||||
onEdit={() => handleEditOperation(selected[0])}
|
|
||||||
onExecute={handleExecuteSelected}
|
|
||||||
onResetPositions={() => setToggleReset(prev => !prev)}
|
onResetPositions={() => setToggleReset(prev => !prev)}
|
||||||
onSavePositions={handleSavePositions}
|
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
{menuProps ? (
|
|
||||||
<NodeContextMenu
|
<NodeContextMenu isOpen={isContextMenuOpen} onHide={() => setIsContextMenuOpen(false)} {...menuProps} />
|
||||||
isOpen={isContextMenuOpen}
|
|
||||||
onHide={handleContextMenuHide}
|
|
||||||
onDelete={handleDeleteOperation}
|
|
||||||
onCreateInput={handleInputCreate}
|
|
||||||
onEditSchema={handleEditSchema}
|
|
||||||
onEditOperation={handleEditOperation}
|
|
||||||
onExecuteOperation={handleOperationExecute}
|
|
||||||
onRelocateConstituents={handleRelocateConstituents}
|
|
||||||
{...menuProps}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<div className='cc-fade-in relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}>
|
<div className='cc-fade-in relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onNodeDoubleClick={handleNodeDoubleClick}
|
|
||||||
edgesFocusable={false}
|
edgesFocusable={false}
|
||||||
nodesFocusable={false}
|
nodesFocusable={false}
|
||||||
fitView
|
fitView
|
||||||
|
@ -300,8 +219,10 @@ export function OssFlow() {
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
snapToGrid={true}
|
snapToGrid={true}
|
||||||
snapGrid={[GRID_SIZE, GRID_SIZE]}
|
snapGrid={[GRID_SIZE, GRID_SIZE]}
|
||||||
|
onClick={() => setIsContextMenuOpen(false)}
|
||||||
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
onNodeContextMenu={handleContextMenu}
|
onNodeContextMenu={handleContextMenu}
|
||||||
onClick={handleCanvasClick}
|
onNodeDragStart={() => setIsContextMenuOpen(false)}
|
||||||
>
|
>
|
||||||
{showGrid ? <Background gap={GRID_SIZE} /> : null}
|
{showGrid ? <Background gap={GRID_SIZE} /> : null}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useReactFlow } from 'reactflow';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
import { BadgeHelp } from '@/features/help/components';
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
|
import { useOperationExecute } from '@/features/oss/backend/useOperationExecute';
|
||||||
|
import { useUpdatePositions } from '@/features/oss/backend/useUpdatePositions';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import {
|
import {
|
||||||
|
@ -20,6 +23,7 @@ import {
|
||||||
IconReset,
|
IconReset,
|
||||||
IconSave
|
IconSave
|
||||||
} from '@/components/Icons';
|
} from '@/components/Icons';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
|
@ -28,28 +32,21 @@ import { useMutatingOss } from '../../../backend/useMutatingOss';
|
||||||
import { useOSSGraphStore } from '../../../stores/ossGraph';
|
import { useOSSGraphStore } from '../../../stores/ossGraph';
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
|
|
||||||
|
import { VIEW_PADDING } from './OssFlow';
|
||||||
|
import { useGetPositions } from './useGetPositions';
|
||||||
|
|
||||||
interface ToolbarOssGraphProps {
|
interface ToolbarOssGraphProps {
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onEdit: () => void;
|
|
||||||
onExecute: () => void;
|
|
||||||
onFitView: () => void;
|
|
||||||
onSavePositions: () => void;
|
|
||||||
onResetPositions: () => void;
|
onResetPositions: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolbarOssGraph({
|
export function ToolbarOssGraph({ onCreate, onDelete, onResetPositions }: ToolbarOssGraphProps) {
|
||||||
onCreate,
|
const { schema, selected, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||||
onDelete,
|
|
||||||
onEdit,
|
|
||||||
onExecute,
|
|
||||||
onFitView,
|
|
||||||
onSavePositions,
|
|
||||||
onResetPositions
|
|
||||||
}: ToolbarOssGraphProps) {
|
|
||||||
const { schema, selected, isMutable, canDelete } = useOssEdit();
|
|
||||||
const isProcessing = useMutatingOss();
|
const isProcessing = useMutatingOss();
|
||||||
|
const { fitView } = useReactFlow();
|
||||||
const selectedOperation = schema.operationByID.get(selected[0]);
|
const selectedOperation = schema.operationByID.get(selected[0]);
|
||||||
|
const getPositions = useGetPositions();
|
||||||
|
|
||||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||||
|
@ -58,6 +55,11 @@ export function ToolbarOssGraph({
|
||||||
const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate);
|
const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate);
|
||||||
const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight);
|
const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight);
|
||||||
|
|
||||||
|
const { updatePositions } = useUpdatePositions();
|
||||||
|
const { operationExecute } = useOperationExecute();
|
||||||
|
|
||||||
|
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
||||||
|
|
||||||
const readyForSynthesis = (() => {
|
const readyForSynthesis = (() => {
|
||||||
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
|
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -79,6 +81,44 @@ export function ToolbarOssGraph({
|
||||||
return true;
|
return true;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
function handleFitView() {
|
||||||
|
fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSavePositions() {
|
||||||
|
const positions = getPositions();
|
||||||
|
void updatePositions({ itemID: schema.id, positions: positions }).then(() => {
|
||||||
|
positions.forEach(item => {
|
||||||
|
const operation = schema.operationByID.get(item.id);
|
||||||
|
if (operation) {
|
||||||
|
operation.position_x = item.position_x;
|
||||||
|
operation.position_y = item.position_y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOperationExecute() {
|
||||||
|
if (selected.length !== 1 || !readyForSynthesis || !selectedOperation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void operationExecute({
|
||||||
|
itemID: schema.id, //
|
||||||
|
data: { target: selectedOperation.id, positions: getPositions() }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditOperation() {
|
||||||
|
if (selected.length !== 1 || !selectedOperation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showEditOperation({
|
||||||
|
oss: schema,
|
||||||
|
target: selectedOperation,
|
||||||
|
positions: getPositions()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center'>
|
<div className='flex flex-col items-center'>
|
||||||
<div className='cc-icons'>
|
<div className='cc-icons'>
|
||||||
|
@ -90,7 +130,7 @@ export function ToolbarOssGraph({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||||
title='Сбросить вид'
|
title='Сбросить вид'
|
||||||
onClick={onFitView}
|
onClick={handleFitView}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'}
|
title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'}
|
||||||
|
@ -137,7 +177,7 @@ export function ToolbarOssGraph({
|
||||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={onSavePositions}
|
onClick={handleSavePositions}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
||||||
|
@ -149,18 +189,18 @@ export function ToolbarOssGraph({
|
||||||
title='Активировать операцию'
|
title='Активировать операцию'
|
||||||
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
||||||
disabled={isProcessing || selected.length !== 1 || !readyForSynthesis}
|
disabled={isProcessing || selected.length !== 1 || !readyForSynthesis}
|
||||||
onClick={onExecute}
|
onClick={handleOperationExecute}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
||||||
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
||||||
disabled={selected.length !== 1 || isProcessing}
|
disabled={selected.length !== 1 || isProcessing}
|
||||||
onClick={onEdit}
|
onClick={handleEditOperation}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
||||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
disabled={selected.length !== 1 || isProcessing || !canDelete(selected[0])}
|
disabled={selected.length !== 1 || isProcessing || !selectedOperation || !canDelete(selectedOperation)}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -54,6 +54,7 @@ export function NodeCore({ node }: NodeCoreProps) {
|
||||||
<div
|
<div
|
||||||
className='h-[34px] w-[144px] flex items-center justify-center'
|
className='h-[34px] w-[144px] flex items-center justify-center'
|
||||||
data-tooltip-id={globalIDs.operation_tooltip}
|
data-tooltip-id={globalIDs.operation_tooltip}
|
||||||
|
data-tooltip-hidden={node.dragging}
|
||||||
onMouseEnter={() => setHover(node.data.operation)}
|
onMouseEnter={() => setHover(node.data.operation)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
|
export function useGetPositions() {
|
||||||
|
const { getNodes } = useReactFlow();
|
||||||
|
return function getPositions() {
|
||||||
|
return getNodes().map(node => ({
|
||||||
|
id: Number(node.id),
|
||||||
|
position_x: node.position.x,
|
||||||
|
position_y: node.position.y
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { useAuthSuspense } from '@/features/auth';
|
||||||
import { Button } from '@/components/Control';
|
import { Button } from '@/components/Control';
|
||||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||||
import { IconChild, IconEdit2 } from '@/components/Icons';
|
import { IconChild, IconEdit2 } from '@/components/Icons';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
import { useMutatingOss } from '../../backend/useMutatingOss';
|
import { useMutatingOss } from '../../backend/useMutatingOss';
|
||||||
|
|
||||||
|
@ -11,12 +12,18 @@ import { useOssEdit } from './OssEditContext';
|
||||||
export function MenuEditOss() {
|
export function MenuEditOss() {
|
||||||
const { isAnonymous } = useAuthSuspense();
|
const { isAnonymous } = useAuthSuspense();
|
||||||
const editMenu = useDropdown();
|
const editMenu = useDropdown();
|
||||||
const { promptRelocateConstituents, isMutable } = useOssEdit();
|
const { schema, isMutable } = useOssEdit();
|
||||||
const isProcessing = useMutatingOss();
|
const isProcessing = useMutatingOss();
|
||||||
|
|
||||||
|
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
|
||||||
|
|
||||||
function handleRelocate() {
|
function handleRelocate() {
|
||||||
editMenu.hide();
|
editMenu.hide();
|
||||||
promptRelocateConstituents(undefined, []);
|
showRelocateConstituents({
|
||||||
|
oss: schema,
|
||||||
|
initialTarget: undefined,
|
||||||
|
positions: []
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAnonymous) {
|
if (isAnonymous) {
|
||||||
|
|
|
@ -9,13 +9,12 @@ import { useDeleteItem } from '@/features/library/backend/useDeleteItem';
|
||||||
import { RSTabID } from '@/features/rsform/pages/RSFormPage/RSEditContext';
|
import { RSTabID } from '@/features/rsform/pages/RSFormPage/RSEditContext';
|
||||||
import { useRoleStore, UserRole } from '@/features/users';
|
import { useRoleStore, UserRole } from '@/features/users';
|
||||||
|
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { promptText } from '@/utils/labels';
|
import { promptText } from '@/utils/labels';
|
||||||
|
|
||||||
import { type IOperationPosition, OperationType } from '../../backend/types';
|
import { type IOperationPosition, OperationType } from '../../backend/types';
|
||||||
import { useOssSuspense } from '../../backend/useOSS';
|
import { useOssSuspense } from '../../backend/useOSS';
|
||||||
import { type IOperationSchema } from '../../models/oss';
|
import { type IOperation, type IOperationSchema } from '../../models/oss';
|
||||||
|
|
||||||
export enum OssTabID {
|
export enum OssTabID {
|
||||||
CARD = 0,
|
CARD = 0,
|
||||||
|
@ -40,15 +39,9 @@ export interface IOssEditContext {
|
||||||
navigateTab: (tab: OssTabID) => void;
|
navigateTab: (tab: OssTabID) => void;
|
||||||
navigateOperationSchema: (target: number) => void;
|
navigateOperationSchema: (target: number) => void;
|
||||||
|
|
||||||
|
canDeleteOperation: (target: IOperation) => boolean;
|
||||||
deleteSchema: () => void;
|
deleteSchema: () => void;
|
||||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||||
|
|
||||||
canDelete: (target: number) => boolean;
|
|
||||||
promptCreateOperation: (props: ICreateOperationPrompt) => void;
|
|
||||||
promptDeleteOperation: (target: number, positions: IOperationPosition[]) => void;
|
|
||||||
promptEditInput: (target: number, positions: IOperationPosition[]) => void;
|
|
||||||
promptEditOperation: (target: number, positions: IOperationPosition[]) => void;
|
|
||||||
promptRelocateConstituents: (target: number | undefined, positions: IOperationPosition[]) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const OssEditContext = createContext<IOssEditContext | null>(null);
|
const OssEditContext = createContext<IOssEditContext | null>(null);
|
||||||
|
@ -81,12 +74,6 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
||||||
|
|
||||||
const [selected, setSelected] = useState<number[]>([]);
|
const [selected, setSelected] = useState<number[]>([]);
|
||||||
|
|
||||||
const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
|
|
||||||
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
|
||||||
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
|
||||||
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
|
|
||||||
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
|
||||||
|
|
||||||
const { deleteItem } = useDeleteItem();
|
const { deleteItem } = useDeleteItem();
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
@ -128,71 +115,11 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function promptCreateOperation({ defaultX, defaultY, inputs, positions, callback }: ICreateOperationPrompt) {
|
function canDeleteOperation(target: IOperation) {
|
||||||
showCreateOperation({
|
if (target.operation_type === OperationType.INPUT) {
|
||||||
oss: schema,
|
|
||||||
defaultX: defaultX,
|
|
||||||
defaultY: defaultY,
|
|
||||||
positions: positions,
|
|
||||||
initialInputs: inputs,
|
|
||||||
onCreate: callback
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function canDelete(target: number) {
|
|
||||||
const operation = schema.operationByID.get(target);
|
|
||||||
if (!operation) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (operation.operation_type === OperationType.INPUT) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return schema.graph.expandOutputs([target]).length === 0;
|
return schema.graph.expandOutputs([target.id]).length === 0;
|
||||||
}
|
|
||||||
|
|
||||||
function promptEditOperation(target: number, positions: IOperationPosition[]) {
|
|
||||||
const operation = schema.operationByID.get(target);
|
|
||||||
if (!operation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showEditOperation({
|
|
||||||
oss: schema,
|
|
||||||
target: operation,
|
|
||||||
positions: positions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptDeleteOperation(target: number, positions: IOperationPosition[]) {
|
|
||||||
const operation = schema.operationByID.get(target);
|
|
||||||
if (!operation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showDeleteOperation({
|
|
||||||
oss: schema,
|
|
||||||
positions: positions,
|
|
||||||
target: operation
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptEditInput(target: number, positions: IOperationPosition[]) {
|
|
||||||
const operation = schema.operationByID.get(target);
|
|
||||||
if (!operation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showEditInput({
|
|
||||||
oss: schema,
|
|
||||||
target: operation,
|
|
||||||
positions: positions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptRelocateConstituents(target: number | undefined, positions: IOperationPosition[]) {
|
|
||||||
const operation = target ? schema.operationByID.get(target) : undefined;
|
|
||||||
showRelocateConstituents({
|
|
||||||
oss: schema,
|
|
||||||
initialTarget: operation,
|
|
||||||
positions: positions
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -201,22 +128,15 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
||||||
schema,
|
schema,
|
||||||
selected,
|
selected,
|
||||||
|
|
||||||
navigateTab,
|
|
||||||
|
|
||||||
deleteSchema,
|
|
||||||
|
|
||||||
isOwned,
|
isOwned,
|
||||||
isMutable,
|
isMutable,
|
||||||
|
|
||||||
setSelected,
|
navigateTab,
|
||||||
|
|
||||||
navigateOperationSchema,
|
navigateOperationSchema,
|
||||||
promptCreateOperation,
|
|
||||||
canDelete,
|
canDeleteOperation,
|
||||||
promptDeleteOperation,
|
deleteSchema,
|
||||||
promptEditInput,
|
setSelected
|
||||||
promptEditOperation,
|
|
||||||
promptRelocateConstituents
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user