F: Improve OSS UI
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled

This commit is contained in:
Ivan 2024-07-26 17:30:37 +03:00
parent 3c961c8192
commit 2eff1b27b9
16 changed files with 327 additions and 73 deletions

View File

@ -44,6 +44,7 @@ class OperationCreateSerializer(serializers.Serializer):
'alias', 'operation_type', 'title', 'sync_text', \ 'alias', 'operation_type', 'title', 'sync_text', \
'comment', 'result', 'position_x', 'position_y' 'comment', 'result', 'position_x', 'position_y'
create_schema = serializers.BooleanField(default=False, required=False)
item_data = OperationData() item_data = OperationData()
arguments = PKField(many=True, queryset=Operation.objects.all(), required=False) arguments = PKField(many=True, queryset=Operation.objects.all(), required=False)
positions = serializers.ListField( positions = serializers.ListField(

View File

@ -207,6 +207,46 @@ class TestOssViewset(EndpointTester):
new_operation = response.data['new_operation'] new_operation = response.data['new_operation']
self.assertEqual(new_operation['result'], self.ks1.model.pk) self.assertEqual(new_operation['result'], self.ks1.model.pk)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_schema(self):
self.populateData()
data = {
'item_data': {
'alias': 'Test4',
'title': 'Test title',
'comment': 'Comment',
'operation_type': OperationType.INPUT
},
'create_schema': True,
'positions': [],
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
schema = LibraryItem.objects.get(pk=new_operation['result'])
self.assertEqual(schema.alias, data['item_data']['alias'])
self.assertEqual(schema.title, data['item_data']['title'])
self.assertEqual(schema.comment, data['item_data']['comment'])
self.assertEqual(schema.visible, False)
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
self.assertEqual(schema.location, self.owned.model.location)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_result(self):
self.populateData()
data = {
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
'positions': [],
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
self.assertEqual(new_operation['result'], self.ks1.model.pk)
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch') @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation(self): def test_delete_operation(self):

View File

@ -98,7 +98,20 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss = m.OperationSchema(self.get_object()) oss = m.OperationSchema(self.get_object())
with transaction.atomic(): with transaction.atomic():
oss.update_positions(serializer.validated_data['positions']) oss.update_positions(serializer.validated_data['positions'])
new_operation = oss.create_operation(**serializer.validated_data['item_data']) data: dict = serializer.validated_data['item_data']
if data['operation_type'] == m.OperationType.INPUT and serializer.validated_data['create_schema']:
schema = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
owner=oss.model.owner,
alias=data['alias'],
title=data['title'],
comment=data['comment'],
visible=False,
access_policy=oss.model.access_policy,
location=oss.model.location
)
data['result'] = schema
new_operation = oss.create_operation(**data)
if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data: if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data:
for argument in serializer.validated_data['arguments']: for argument in serializer.validated_data['arguments']:
oss.add_argument(operation=new_operation, argument=argument) oss.add_argument(operation=new_operation, argument=argument)

View File

@ -1,31 +1,34 @@
import Tooltip from '@/components/ui/Tooltip'; import Tooltip from '@/components/ui/Tooltip';
import { IOperation } from '@/models/oss'; import { OssNodeInternal } from '@/models/miscellaneous';
import { labelOperationType } from '@/utils/labels'; import { labelOperationType } from '@/utils/labels';
interface TooltipOperationProps { interface TooltipOperationProps {
data: IOperation; node: OssNodeInternal;
anchor: string; anchor: string;
} }
function TooltipOperation({ data, anchor }: TooltipOperationProps) { function TooltipOperation({ node, anchor }: TooltipOperationProps) {
return ( return (
<Tooltip layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[30rem] dense'> <Tooltip layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[35rem] max-h-[40rem] dense my-3'>
<h2>Операция {data.alias}</h2> <h2>{node.data.operation.alias}</h2>
<p> <p>
<b>Тип:</b> {labelOperationType(data.operation_type)} <b>Тип:</b> {labelOperationType(node.data.operation.operation_type)}
</p> </p>
{data.title ? ( {node.data.operation.title ? (
<p> <p>
<b>Название: </b> <b>Название: </b>
{data.title} {node.data.operation.title}
</p> </p>
) : null} ) : null}
{data.comment ? ( {node.data.operation.comment ? (
<p> <p>
<b>Комментарий: </b> <b>Комментарий: </b>
{data.comment} {node.data.operation.comment}
</p> </p>
) : null} ) : null}
<p>
<b>Положение:</b> [{node.xPos}, {node.yPos}]
</p>
</Tooltip> </Tooltip>
); );
} }

View File

@ -41,8 +41,12 @@ function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCrea
const [inputs, setInputs] = useState<OperationID[]>([]); const [inputs, setInputs] = useState<OperationID[]>([]);
const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined); const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined);
const [syncText, setSyncText] = useState(true); const [syncText, setSyncText] = useState(true);
const [createSchema, setCreateSchema] = useState(false);
const isValid = useMemo(() => alias !== '', [alias]); const isValid = useMemo(
() => (alias !== '' && activeTab === TabID.INPUT) || inputs.length != 1,
[alias, activeTab, inputs]
);
useLayoutEffect(() => { useLayoutEffect(() => {
if (attachedID) { if (attachedID) {
@ -68,7 +72,8 @@ function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCrea
result: activeTab === TabID.INPUT ? attachedID ?? null : null result: activeTab === TabID.INPUT ? attachedID ?? null : null
}, },
positions: positions, positions: positions,
arguments: activeTab === TabID.INPUT ? undefined : inputs.length > 0 ? inputs : undefined arguments: activeTab === TabID.INPUT ? undefined : inputs.length > 0 ? inputs : undefined,
create_schema: createSchema
}; };
onCreate(data); onCreate(data);
}; };
@ -88,10 +93,12 @@ function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCrea
setAttachedID={setAttachedID} setAttachedID={setAttachedID}
syncText={syncText} syncText={syncText}
setSyncText={setSyncText} setSyncText={setSyncText}
createSchema={createSchema}
setCreateSchema={setCreateSchema}
/> />
</TabPanel> </TabPanel>
), ),
[alias, comment, title, attachedID, syncText, oss] [alias, comment, title, attachedID, syncText, oss, createSchema]
); );
const synthesisPanel = useMemo( const synthesisPanel = useMemo(
@ -105,11 +112,12 @@ function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCrea
setComment={setComment} setComment={setComment}
title={title} title={title}
setTitle={setTitle} setTitle={setTitle}
inputs={inputs}
setInputs={setInputs} setInputs={setInputs}
/> />
</TabPanel> </TabPanel>
), ),
[oss, alias, comment, title] [oss, alias, comment, title, inputs]
); );
return ( return (

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback } from 'react'; import { useCallback, useEffect } from 'react';
import { IconReset } from '@/components/Icons'; import { IconReset } from '@/components/Icons';
import PickSchema from '@/components/select/PickSchema'; import PickSchema from '@/components/select/PickSchema';
@ -27,6 +27,8 @@ interface TabInputOperationProps {
setAttachedID: React.Dispatch<React.SetStateAction<LibraryItemID | undefined>>; setAttachedID: React.Dispatch<React.SetStateAction<LibraryItemID | undefined>>;
syncText: boolean; syncText: boolean;
setSyncText: React.Dispatch<React.SetStateAction<boolean>>; setSyncText: React.Dispatch<React.SetStateAction<boolean>>;
createSchema: boolean;
setCreateSchema: React.Dispatch<React.SetStateAction<boolean>>;
} }
function TabInputOperation({ function TabInputOperation({
@ -40,10 +42,19 @@ function TabInputOperation({
attachedID, attachedID,
setAttachedID, setAttachedID,
syncText, syncText,
setSyncText setSyncText,
createSchema,
setCreateSchema
}: TabInputOperationProps) { }: TabInputOperationProps) {
const baseFilter = useCallback((item: ILibraryItem) => !oss.schemas.includes(item.id), [oss]); const baseFilter = useCallback((item: ILibraryItem) => !oss.schemas.includes(item.id), [oss]);
useEffect(() => {
if (createSchema) {
setAttachedID(undefined);
setSyncText(true);
}
}, [createSchema, setAttachedID, setSyncText]);
return ( return (
<AnimateFade className='cc-column'> <AnimateFade className='cc-column'>
<TextInput <TextInput
@ -84,7 +95,8 @@ function TabInputOperation({
/> />
</div> </div>
<div className='flex gap-3 items-center'> <div className='flex justify-between gap-3 items-center'>
<div className='flex gap-3'>
<Label text='Загружаемая концептуальная схема' /> <Label text='Загружаемая концептуальная схема' />
<MiniButton <MiniButton
title='Сбросить выбор схемы' title='Сбросить выбор схемы'
@ -95,13 +107,21 @@ function TabInputOperation({
disabled={attachedID == undefined} disabled={attachedID == undefined}
/> />
</div> </div>
<Checkbox
value={createSchema}
setValue={setCreateSchema}
label='Создать новую схему'
titleHtml='Создать пустую схему для загрузки'
/>
</div>
{!createSchema ? (
<PickSchema <PickSchema
value={attachedID} // prettier: split-line value={attachedID} // prettier: split-line
onSelectValue={setAttachedID} onSelectValue={setAttachedID}
rows={8} rows={8}
baseFilter={baseFilter} baseFilter={baseFilter}
/> />
) : null}
</AnimateFade> </AnimateFade>
); );
} }

View File

@ -19,6 +19,7 @@ interface TabSynthesisOperationProps {
setTitle: React.Dispatch<React.SetStateAction<string>>; setTitle: React.Dispatch<React.SetStateAction<string>>;
comment: string; comment: string;
setComment: React.Dispatch<React.SetStateAction<string>>; setComment: React.Dispatch<React.SetStateAction<string>>;
inputs: OperationID[];
setInputs: React.Dispatch<React.SetStateAction<OperationID[]>>; setInputs: React.Dispatch<React.SetStateAction<OperationID[]>>;
} }
@ -30,11 +31,14 @@ function TabSynthesisOperation({
setTitle, setTitle,
comment, comment,
setComment, setComment,
inputs,
setInputs setInputs
}: TabSynthesisOperationProps) { }: TabSynthesisOperationProps) {
const [left, setLeft] = useState<IOperation | undefined>(undefined); const [left, setLeft] = useState<IOperation | undefined>(undefined);
const [right, setRight] = useState<IOperation | undefined>(undefined); const [right, setRight] = useState<IOperation | undefined>(undefined);
console.log(inputs);
useEffect(() => { useEffect(() => {
const inputs: OperationID[] = []; const inputs: OperationID[] = [];
if (left) { if (left) {

View File

@ -2,7 +2,10 @@
* Module: Miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules. * Module: Miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
*/ */
import { Node } from 'reactflow';
import { LibraryItemType, LocationHead } from './library'; import { LibraryItemType, LocationHead } from './library';
import { IOperation } from './oss';
/** /**
* Represents graph dependency mode. * Represents graph dependency mode.
@ -16,6 +19,31 @@ export enum DependencyMode {
EXPAND_INPUTS EXPAND_INPUTS
} }
/**
* Represents graph OSS node data.
*/
export interface OssNode extends Node {
id: string;
data: {
label: string;
operation: IOperation;
};
position: { x: number; y: number };
}
/**
* Represents graph OSS node internal data.
*/
export interface OssNodeInternal {
id: string;
data: {
label: string;
operation: IOperation;
};
xPos: number;
yPos: number;
}
/** /**
* Represents graph node coloring scheme. * Represents graph node coloring scheme.
*/ */

View File

@ -66,6 +66,7 @@ export interface IOperationCreateData extends IPositionsData {
'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y' | 'result' | 'sync_text' 'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y' | 'result' | 'sync_text'
>; >;
arguments: OperationID[] | undefined; arguments: OperationID[] | undefined;
create_schema: boolean;
} }
/** /**

View File

@ -4,26 +4,17 @@ import { IconRSForm } from '@/components/Icons';
import TooltipOperation from '@/components/info/TooltipOperation'; import TooltipOperation from '@/components/info/TooltipOperation';
import MiniButton from '@/components/ui/MiniButton.tsx'; import MiniButton from '@/components/ui/MiniButton.tsx';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import { IOperation } from '@/models/oss'; import { OssNodeInternal } from '@/models/miscellaneous';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
interface InputNodeProps { function InputNode(node: OssNodeInternal) {
id: string;
data: {
label: string;
operation: IOperation;
};
}
function InputNode({ id, data }: InputNodeProps) {
const controller = useOssEdit(); const controller = useOssEdit();
const hasFile = !!node.data.operation.result;
const hasFile = !!data.operation.result;
const handleOpenSchema = () => { const handleOpenSchema = () => {
controller.openOperationSchema(Number(id)); controller.openOperationSchema(Number(node.id));
}; };
return ( return (
@ -41,10 +32,10 @@ function InputNode({ id, data }: InputNodeProps) {
disabled={!hasFile} disabled={!hasFile}
/> />
</Overlay> </Overlay>
<div id={`${prefixes.operation_list}${id}`} className='flex-grow text-center text-sm'> <div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center text-sm'>
{data.label} {node.data.label}
{controller.showTooltip ? ( {controller.showTooltip ? (
<TooltipOperation anchor={`#${prefixes.operation_list}${id}`} data={data.operation} /> <TooltipOperation anchor={`#${prefixes.operation_list}${node.id}`} node={node} />
) : null} ) : null}
</div> </div>
</> </>

View File

@ -0,0 +1,110 @@
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { IconDestroy, IconEdit2, IconNewItem, IconRSForm } from '@/components/Icons';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import useClickedOutside from '@/hooks/useClickedOutside';
import { IOperation, OperationID, OperationType } from '@/models/oss';
import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/labels';
import { useOssEdit } from '../OssEditContext';
export interface ContextMenuData {
operation: IOperation;
cursorX: number;
cursorY: number;
}
interface NodeContextMenuProps extends ContextMenuData {
onHide: () => void;
onDelete: (target: OperationID) => void;
}
function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete }: NodeContextMenuProps) {
const controller = useOssEdit();
const [isOpen, setIsOpen] = useState(false);
const ref = useRef(null);
const handleHide = useCallback(() => {
setIsOpen(false);
onHide();
}, [onHide]);
useClickedOutside({ ref, callback: handleHide });
useEffect(() => setIsOpen(true), []);
const handleOpenSchema = () => {
controller.openOperationSchema(operation.id);
};
const handleEditSchema = () => {
toast.error('Not implemented');
handleHide();
};
const handleEditOperation = () => {
toast.error('Not implemented');
handleHide();
};
const handleDeleteOperation = () => {
handleHide();
onDelete(operation.id);
};
return (
<div ref={ref} className='absolute' style={{ top: cursorY, left: cursorX, width: 10, height: 10 }}>
<Dropdown isOpen={isOpen} stretchLeft={cursorX >= window.innerWidth - PARAMETER.ossContextMenuWidth}>
<DropdownButton
text='Свойства операции'
titleHtml={prepareTooltip('Редактировать операцию', 'Ctrl + клик')}
icon={<IconEdit2 size='1rem' className='icon-primary' />}
disabled={controller.isProcessing}
onClick={handleEditOperation}
/>
{operation.result ? (
<DropdownButton
text='Открыть схему'
title='Открыть привязанную КС'
icon={<IconRSForm size='1rem' className='icon-green' />}
disabled={controller.isProcessing}
onClick={handleOpenSchema}
/>
) : null}
{controller.isMutable && !operation.result && operation.operation_type === OperationType.INPUT ? (
<DropdownButton
text='Создать схему'
title='Создать пустую схему для загрузки'
icon={<IconNewItem size='1rem' className='icon-green' />}
disabled={controller.isProcessing}
onClick={handleEditSchema}
/>
) : null}
{controller.isMutable && operation.operation_type === OperationType.INPUT ? (
<DropdownButton
text='Привязать схему'
title='Выбрать схему для загрузки'
icon={<IconRSForm size='1rem' className='icon-primary' />}
disabled={controller.isProcessing}
onClick={handleEditSchema}
/>
) : null}
<DropdownButton
text='Удалить операцию'
icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={!controller.isMutable || controller.isProcessing}
onClick={handleDeleteOperation}
/>
</Dropdown>
</div>
);
}
export default NodeContextMenu;

View File

@ -4,25 +4,18 @@ import { IconRSForm } from '@/components/Icons';
import TooltipOperation from '@/components/info/TooltipOperation'; import TooltipOperation from '@/components/info/TooltipOperation';
import MiniButton from '@/components/ui/MiniButton.tsx'; import MiniButton from '@/components/ui/MiniButton.tsx';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import { IOperation } from '@/models/oss'; import { OssNodeInternal } from '@/models/miscellaneous';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
interface OperationNodeProps {
id: string;
data: {
label: string;
operation: IOperation;
};
}
function OperationNode({ id, data }: OperationNodeProps) { function OperationNode(node: OssNodeInternal) {
const controller = useOssEdit(); const controller = useOssEdit();
const hasFile = !!data.operation.result; const hasFile = !!node.data.operation.result;
const handleOpenSchema = () => { const handleOpenSchema = () => {
controller.openOperationSchema(Number(id)); controller.openOperationSchema(Number(node.id));
}; };
return ( return (
@ -41,9 +34,9 @@ function OperationNode({ id, data }: OperationNodeProps) {
/> />
</Overlay> </Overlay>
<div id={`${prefixes.operation_list}${id}`} className='flex-grow text-center text-sm'> <div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center text-sm'>
{data.label} {node.data.label}
<TooltipOperation anchor={`#${prefixes.operation_list}${id}`} data={data.operation} /> <TooltipOperation anchor={`#${prefixes.operation_list}${node.id}`} node={node} />
</div> </div>
<Handle type='target' position={Position.Top} id='left' style={{ left: 40 }} /> <Handle type='target' position={Position.Top} id='left' style={{ left: 40 }} />

View File

@ -18,15 +18,19 @@ import {
useReactFlow useReactFlow
} from 'reactflow'; } from 'reactflow';
import { CProps } from '@/components/props';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useOSS } from '@/context/OssContext'; import { useOSS } from '@/context/OssContext';
import { OssNode } from '@/models/miscellaneous';
import { OperationID } from '@/models/oss';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { errors } from '@/utils/labels'; import { errors } from '@/utils/labels';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
import InputNode from './InputNode'; import InputNode from './InputNode';
import NodeContextMenu, { ContextMenuData } from './NodeContextMenu';
import OperationNode from './OperationNode'; import OperationNode from './OperationNode';
import ToolbarOssGraph from './ToolbarOssGraph'; import ToolbarOssGraph from './ToolbarOssGraph';
@ -46,6 +50,7 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
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 | undefined>(undefined);
const onSelectionChange = useCallback( const onSelectionChange = useCallback(
({ nodes }: { nodes: Node[] }) => { ({ nodes }: { nodes: Node[] }) => {
@ -118,13 +123,20 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
controller.promptCreateOperation(center.x, center.y, getPositions()); controller.promptCreateOperation(center.x, center.y, getPositions());
}, [controller, getPositions, flow]); }, [controller, getPositions, flow]);
const handleDeleteOperation = useCallback(() => { const handleDeleteSelected = useCallback(() => {
if (controller.selected.length !== 1) { if (controller.selected.length !== 1) {
return; return;
} }
controller.deleteOperation(controller.selected[0], getPositions()); controller.deleteOperation(controller.selected[0], getPositions());
}, [controller, getPositions]); }, [controller, getPositions]);
const handleDeleteOperation = useCallback(
(target: OperationID) => {
controller.deleteOperation(target, getPositions());
},
[controller, getPositions]
);
const handleFitView = useCallback(() => { const handleFitView = useCallback(() => {
flow.fitView({ duration: PARAMETER.zoomDuration }); flow.fitView({ duration: PARAMETER.zoomDuration });
}, [flow]); }, [flow]);
@ -167,15 +179,30 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
}, [colors, nodes]); }, [colors, nodes]);
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => { (event: CProps.EventMouse, node: OssNode) => {
console.log(node);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
controller.setShowTooltip(prev => !prev);
// setShowContextMenu(true); setMenuProps({
operation: node.data.operation,
cursorX: event.clientX,
cursorY: event.clientY
});
controller.setShowTooltip(false);
}, },
[controller] [controller]
); );
const handleContextMenuHide = useCallback(() => {
controller.setShowTooltip(true);
setMenuProps(undefined);
}, [controller]);
const handleClickCanvas = useCallback(() => {
handleContextMenuHide();
}, [handleContextMenuHide]);
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (controller.isProcessing) { if (controller.isProcessing) {
return; return;
@ -192,7 +219,7 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
if (event.key === 'Delete') { if (event.key === 'Delete') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
handleDeleteOperation(); handleDeleteSelected();
return; return;
} }
} }
@ -224,12 +251,23 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
nodesConnectable={false} nodesConnectable={false}
snapToGrid={true} snapToGrid={true}
snapGrid={[10, 10]} snapGrid={[10, 10]}
onContextMenu={handleContextMenu} onNodeContextMenu={handleContextMenu}
onClick={handleClickCanvas}
> >
{showGrid ? <Background gap={10} /> : null} {showGrid ? <Background gap={10} /> : null}
</ReactFlow> </ReactFlow>
), ),
[nodes, edges, proOptions, handleNodesChange, handleContextMenu, onEdgesChange, OssNodeTypes, showGrid] [
nodes,
edges,
proOptions,
handleNodesChange,
handleContextMenu,
handleClickCanvas,
onEdgesChange,
OssNodeTypes,
showGrid
]
); );
return ( return (
@ -240,13 +278,16 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
showGrid={showGrid} showGrid={showGrid}
onFitView={handleFitView} onFitView={handleFitView}
onCreate={handleCreateOperation} onCreate={handleCreateOperation}
onDelete={handleDeleteOperation} onDelete={handleDeleteSelected}
onResetPositions={handleResetPositions} onResetPositions={handleResetPositions}
onSavePositions={handleSavePositions} onSavePositions={handleSavePositions}
onSaveImage={handleSaveImage} onSaveImage={handleSaveImage}
toggleShowGrid={() => setShowGrid(prev => !prev)} toggleShowGrid={() => setShowGrid(prev => !prev)}
/> />
</Overlay> </Overlay>
{menuProps ? (
<NodeContextMenu onHide={handleContextMenuHide} onDelete={handleDeleteOperation} {...menuProps} />
) : null}
<div className='relative' style={{ height: canvasHeight, width: canvasWidth }}> <div className='relative' style={{ height: canvasHeight, width: canvasWidth }}>
{graph} {graph}
</div> </div>

View File

@ -62,7 +62,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
); );
return ( return (
<div className='flex border-b clr-input'> <div className='flex border-b clr-input overflow-hidden'>
<SearchBar <SearchBar
id='constituents_search' id='constituents_search'
noBorder noBorder

View File

@ -57,7 +57,7 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit
className={clsx( className={clsx(
'border overflow-visible', // prettier: split-lines 'border overflow-visible', // prettier: split-lines
{ {
'mt-[2.2rem] rounded-l-md rounded-r-none': !isBottom, // prettier: split-lines 'mt-[2.2rem] rounded-l-md rounded-r-none': !isBottom,
'mt-3 mx-6 rounded-md md:w-[45.8rem]': isBottom 'mt-3 mx-6 rounded-md md:w-[45.8rem]': isBottom
} }
)} )}

View File

@ -14,6 +14,7 @@ export const PARAMETER = {
zoomDuration: 500, // milliseconds animation duration zoomDuration: 500, // milliseconds animation duration
ossImageWidth: 1280, // pixels - size of OSS image ossImageWidth: 1280, // pixels - size of OSS image
ossImageHeight: 960, // pixels - size of OSS image ossImageHeight: 960, // pixels - size of OSS image
ossContextMenuWidth: 200, // pixels - width of OSS context menu
graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be
graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be