mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 21:10:38 +03:00
F: Implement OSS context menu
This commit is contained in:
parent
473f17cfa6
commit
083f6bf299
|
@ -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(
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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;
|
|
@ -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 }} />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user