mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Improve OSS frontend
This commit is contained in:
parent
1bcf660c15
commit
bf7902258b
|
@ -4,7 +4,7 @@ from rest_framework import serializers
|
||||||
|
|
||||||
from shared import messages as msg
|
from shared import messages as msg
|
||||||
|
|
||||||
from ..models import Constituenta, LibraryItem, RSForm
|
from ..models import Constituenta, RSForm
|
||||||
from ..utils import fix_old_references
|
from ..utils import fix_old_references
|
||||||
|
|
||||||
_CST_TYPE = 'constituenta'
|
_CST_TYPE = 'constituenta'
|
||||||
|
|
|
@ -13,7 +13,9 @@ import {
|
||||||
ICstSubstituteData,
|
ICstSubstituteData,
|
||||||
IOperationCreateData,
|
IOperationCreateData,
|
||||||
IOperationCreatedResponse,
|
IOperationCreatedResponse,
|
||||||
IOperationSchemaData
|
IOperationSchemaData,
|
||||||
|
IPositionsData,
|
||||||
|
ITargetOperation
|
||||||
} from '@/models/oss';
|
} from '@/models/oss';
|
||||||
import {
|
import {
|
||||||
IConstituentaList,
|
IConstituentaList,
|
||||||
|
@ -424,6 +426,13 @@ export function getOssDetails(target: string, request: FrontPull<IOperationSchem
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function patchUpdatePositions(schema: string, request: FrontPush<IPositionsData>) {
|
||||||
|
AxiosPatch({
|
||||||
|
endpoint: `/api/oss/${schema}/update-positions`,
|
||||||
|
request: request
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function postCreateOperation(
|
export function postCreateOperation(
|
||||||
schema: string,
|
schema: string,
|
||||||
request: FrontExchange<IOperationCreateData, IOperationCreatedResponse>
|
request: FrontExchange<IOperationCreateData, IOperationCreatedResponse>
|
||||||
|
@ -434,6 +443,13 @@ export function postCreateOperation(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function patchDeleteOperation(schema: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) {
|
||||||
|
AxiosPatch({
|
||||||
|
endpoint: `/api/oss/${schema}/delete-operation`,
|
||||||
|
request: request
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function postInflectText(request: FrontExchange<IWordFormPlain, ITextResult>) {
|
export function postInflectText(request: FrontExchange<IWordFormPlain, ITextResult>) {
|
||||||
AxiosPost({
|
AxiosPost({
|
||||||
endpoint: `/api/cctext/inflect`,
|
endpoint: `/api/cctext/inflect`,
|
||||||
|
|
|
@ -5,11 +5,13 @@ import { createContext, useCallback, useContext, useMemo, useState } from 'react
|
||||||
import {
|
import {
|
||||||
type DataCallback,
|
type DataCallback,
|
||||||
deleteUnsubscribe,
|
deleteUnsubscribe,
|
||||||
|
patchDeleteOperation,
|
||||||
patchEditorsSet as patchSetEditors,
|
patchEditorsSet as patchSetEditors,
|
||||||
patchLibraryItem,
|
patchLibraryItem,
|
||||||
patchSetAccessPolicy,
|
patchSetAccessPolicy,
|
||||||
patchSetLocation,
|
patchSetLocation,
|
||||||
patchSetOwner,
|
patchSetOwner,
|
||||||
|
patchUpdatePositions,
|
||||||
postCreateOperation,
|
postCreateOperation,
|
||||||
postSubscribe
|
postSubscribe
|
||||||
} from '@/app/backendAPI';
|
} from '@/app/backendAPI';
|
||||||
|
@ -17,7 +19,7 @@ import { type ErrorData } from '@/components/info/InfoError';
|
||||||
import useOssDetails from '@/hooks/useOssDetails';
|
import useOssDetails from '@/hooks/useOssDetails';
|
||||||
import { AccessPolicy, ILibraryItem } from '@/models/library';
|
import { AccessPolicy, ILibraryItem } from '@/models/library';
|
||||||
import { ILibraryUpdateData } from '@/models/library';
|
import { ILibraryUpdateData } from '@/models/library';
|
||||||
import { IOperation, IOperationCreateData, IOperationSchema } from '@/models/oss';
|
import { IOperation, IOperationCreateData, IOperationSchema, IPositionsData, ITargetOperation } from '@/models/oss';
|
||||||
import { UserID } from '@/models/user';
|
import { UserID } from '@/models/user';
|
||||||
import { contextOutsideScope } from '@/utils/labels';
|
import { contextOutsideScope } from '@/utils/labels';
|
||||||
|
|
||||||
|
@ -45,7 +47,9 @@ interface IOssContext {
|
||||||
setLocation: (newLocation: string, callback?: () => void) => void;
|
setLocation: (newLocation: string, callback?: () => void) => void;
|
||||||
setEditors: (newEditors: UserID[], callback?: () => void) => void;
|
setEditors: (newEditors: UserID[], callback?: () => void) => void;
|
||||||
|
|
||||||
|
savePositions: (data: IPositionsData, callback?: () => void) => void;
|
||||||
createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void;
|
createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void;
|
||||||
|
deleteOperation: (data: ITargetOperation, callback?: () => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OssContext = createContext<IOssContext | null>(null);
|
const OssContext = createContext<IOssContext | null>(null);
|
||||||
|
@ -250,6 +254,23 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
|
||||||
[itemID, schema]
|
[itemID, schema]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const savePositions = useCallback(
|
||||||
|
(data: IPositionsData, callback?: () => void) => {
|
||||||
|
setProcessingError(undefined);
|
||||||
|
patchUpdatePositions(itemID, {
|
||||||
|
data: data,
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: setProcessingError,
|
||||||
|
onSuccess: () => {
|
||||||
|
library.localUpdateTimestamp(Number(itemID));
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[itemID, library]
|
||||||
|
);
|
||||||
|
|
||||||
const createOperation = useCallback(
|
const createOperation = useCallback(
|
||||||
(data: IOperationCreateData, callback?: DataCallback<IOperation>) => {
|
(data: IOperationCreateData, callback?: DataCallback<IOperation>) => {
|
||||||
setProcessingError(undefined);
|
setProcessingError(undefined);
|
||||||
|
@ -268,6 +289,24 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
|
||||||
[itemID, library, setSchema]
|
[itemID, library, setSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const deleteOperation = useCallback(
|
||||||
|
(data: ITargetOperation, callback?: () => void) => {
|
||||||
|
setProcessingError(undefined);
|
||||||
|
patchDeleteOperation(itemID, {
|
||||||
|
data: data,
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: setProcessingError,
|
||||||
|
onSuccess: newData => {
|
||||||
|
setSchema(newData);
|
||||||
|
library.localUpdateTimestamp(newData.id);
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[itemID, library, setSchema]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OssContext.Provider
|
<OssContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -288,7 +327,9 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
|
||||||
setAccessPolicy,
|
setAccessPolicy,
|
||||||
setLocation,
|
setLocation,
|
||||||
|
|
||||||
createOperation
|
savePositions,
|
||||||
|
createOperation,
|
||||||
|
deleteOperation
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -41,16 +41,29 @@ export interface IOperation {
|
||||||
*/
|
*/
|
||||||
export interface IOperationPosition extends Pick<IOperation, 'id' | 'position_x' | 'position_y'> {}
|
export interface IOperationPosition extends Pick<IOperation, 'id' | 'position_x' | 'position_y'> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents all {@link IOperation} positions in {@link IOperationSchema}.
|
||||||
|
*/
|
||||||
|
export interface IPositionsData {
|
||||||
|
positions: IOperationPosition[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents target {@link IOperation}.
|
||||||
|
*/
|
||||||
|
export interface ITargetOperation extends IPositionsData {
|
||||||
|
target: OperationID;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents {@link IOperation} data, used in creation process.
|
* Represents {@link IOperation} data, used in creation process.
|
||||||
*/
|
*/
|
||||||
export interface IOperationCreateData {
|
export interface IOperationCreateData extends IPositionsData {
|
||||||
item_data: Pick<
|
item_data: Pick<
|
||||||
IOperation,
|
IOperation,
|
||||||
'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y' | 'result'
|
'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y' | 'result'
|
||||||
>;
|
>;
|
||||||
arguments: OperationID[] | undefined;
|
arguments: OperationID[] | undefined;
|
||||||
positions: IOperationPosition[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,10 +4,15 @@ import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
import OssFlow from './OssFlow';
|
import OssFlow from './OssFlow';
|
||||||
|
|
||||||
function EditorOssGraph() {
|
interface EditorOssGraphProps {
|
||||||
|
isModified: boolean;
|
||||||
|
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) {
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<OssFlow />
|
<OssFlow isModified={isModified} setIsModified={setIsModified} />
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { CiSquareRemove } from 'react-icons/ci';
|
|
||||||
import { PiPlugsConnected } from 'react-icons/pi';
|
|
||||||
import { Handle, Position } from 'reactflow';
|
import { Handle, Position } from 'reactflow';
|
||||||
|
|
||||||
|
import { IconDestroy, IconEdit2 } from '@/components/Icons';
|
||||||
import MiniButton from '@/components/ui/MiniButton.tsx';
|
import MiniButton from '@/components/ui/MiniButton.tsx';
|
||||||
|
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
|
@ -21,27 +20,30 @@ function InputNode({ id, data }: InputNodeProps) {
|
||||||
console.log('delete node ' + id);
|
console.log('delete node ' + id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleEditOperation = () => {
|
||||||
|
console.log('edit operation ' + id);
|
||||||
//controller.selectNode(id);
|
//controller.selectNode(id);
|
||||||
// controller.showSelectInput();
|
//controller.showSynthesis();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Handle type='target' position={Position.Bottom} />
|
<Handle type='source' position={Position.Bottom} />
|
||||||
<div className='flex justify-between'>
|
<div className='flex justify-between items-center'>
|
||||||
<div className='flex-grow text-center'>{data.label}</div>
|
<div className='flex-grow text-center'>{data.label}</div>
|
||||||
<div className='cc-icons'>
|
<div className='cc-icons'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<PiPlugsConnected className='icon-green' />}
|
icon={<IconEdit2 className='icon-primary' size='0.75rem' />}
|
||||||
title='Привязать схему'
|
noPadding
|
||||||
|
title='Редактировать'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleClick();
|
handleEditOperation();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<CiSquareRemove className='icon-red' size='1rem' />}
|
noPadding
|
||||||
title='Удалить'
|
icon={<IconDestroy className='icon-red' size='0.75rem' />}
|
||||||
|
title='Удалить операцию'
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { CiSquareRemove } from 'react-icons/ci';
|
|
||||||
import { IoGitNetworkSharp } from 'react-icons/io5';
|
|
||||||
import { VscDebugStart } from 'react-icons/vsc';
|
import { VscDebugStart } from 'react-icons/vsc';
|
||||||
import { Handle, Position } from 'reactflow';
|
import { Handle, Position } from 'reactflow';
|
||||||
|
|
||||||
|
import { IconDestroy, IconEdit2 } from '@/components/Icons';
|
||||||
import MiniButton from '@/components/ui/MiniButton.tsx';
|
import MiniButton from '@/components/ui/MiniButton.tsx';
|
||||||
|
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
interface OperationNodeProps {
|
interface OperationNodeProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
data: {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function OperationNode({ id }: OperationNodeProps) {
|
function OperationNode({ id, data }: OperationNodeProps) {
|
||||||
const controller = useOssEdit();
|
const controller = useOssEdit();
|
||||||
console.log(controller.isMutable);
|
console.log(controller.isMutable);
|
||||||
|
|
||||||
|
@ -32,37 +34,33 @@ function OperationNode({ id }: OperationNodeProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Handle type='target' position={Position.Bottom} />
|
<Handle type='source' position={Position.Bottom} />
|
||||||
<div>
|
<div className='flex justify-between'>
|
||||||
|
<div className='flex-grow text-center'>{data.label}</div>
|
||||||
|
<div className='cc-icons'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
className='float-right'
|
icon={<IconEdit2 className='icon-primary' size='1rem' />}
|
||||||
icon={<CiSquareRemove className='icon-red' />}
|
title='Редактировать'
|
||||||
title='Удалить'
|
onClick={() => {
|
||||||
onClick={handleDelete}
|
handleEditOperation();
|
||||||
color={'red'}
|
}}
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
Тип: <strong>Отождествление</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Схема: <strong></strong>
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
className='float-right'
|
className='float-right'
|
||||||
icon={<VscDebugStart className='icon-green' />}
|
icon={<VscDebugStart className='icon-green' size='1rem' />}
|
||||||
title='Синтез'
|
title='Синтез'
|
||||||
onClick={() => handleRunOperation()}
|
onClick={() => handleRunOperation()}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
className='float-right'
|
icon={<IconDestroy className='icon-red' size='1rem' />}
|
||||||
icon={<IoGitNetworkSharp className='icon-green' />}
|
title='Удалить операцию'
|
||||||
title='Отождествления'
|
onClick={handleDelete}
|
||||||
onClick={() => handleEditOperation()}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle type='source' position={Position.Top} id='a' style={{ left: 50 }} />
|
<Handle type='target' position={Position.Top} id='left' style={{ left: 40 }} />
|
||||||
<Handle type='source' position={Position.Top} id='b' style={{ right: 50, left: 'auto' }} />
|
<Handle type='target' position={Position.Top} id='right' style={{ right: 40, left: 'auto' }} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +1,61 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useLayoutEffect, useMemo } from 'react';
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
EdgeChange,
|
Node,
|
||||||
NodeChange,
|
NodeChange,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
ProOptions,
|
ProOptions,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
useNodesState,
|
useNodesState,
|
||||||
useViewport
|
useOnSelectionChange,
|
||||||
|
useReactFlow
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
|
|
||||||
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 { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
import InputNode from './InputNode';
|
import InputNode from './InputNode';
|
||||||
import OperationNode from './OperationNode';
|
import OperationNode from './OperationNode';
|
||||||
import ToolbarOssGraph from './ToolbarOssGraph';
|
import ToolbarOssGraph from './ToolbarOssGraph';
|
||||||
|
|
||||||
function OssFlow() {
|
interface OssFlowProps {
|
||||||
|
isModified: boolean;
|
||||||
|
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
const { calculateHeight } = useConceptOptions();
|
const { calculateHeight } = useConceptOptions();
|
||||||
const model = useOSS();
|
const model = useOSS();
|
||||||
const controller = useOssEdit();
|
const controller = useOssEdit();
|
||||||
const viewport = useViewport();
|
const flow = useReactFlow();
|
||||||
|
|
||||||
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 onSelectionChange = useCallback(
|
||||||
|
({ nodes }: { nodes: Node[] }) => {
|
||||||
|
controller.setSelected(nodes.map(node => Number(node.id)));
|
||||||
|
console.log(nodes);
|
||||||
|
},
|
||||||
|
[controller]
|
||||||
|
);
|
||||||
|
|
||||||
|
useOnSelectionChange({
|
||||||
|
onChange: onSelectionChange
|
||||||
|
});
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!model.schema) {
|
if (!model.schema) {
|
||||||
setNodes([]);
|
setNodes([]);
|
||||||
setEdges([]);
|
setEdges([]);
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
setNodes(
|
setNodes(
|
||||||
model.schema.items.map(operation => ({
|
model.schema.items.map(operation => ({
|
||||||
id: String(operation.id),
|
id: String(operation.id),
|
||||||
|
@ -49,10 +68,19 @@ function OssFlow() {
|
||||||
model.schema.arguments.map((argument, index) => ({
|
model.schema.arguments.map((argument, index) => ({
|
||||||
id: String(index),
|
id: String(index),
|
||||||
source: String(argument.argument),
|
source: String(argument.argument),
|
||||||
target: String(argument.operation)
|
target: String(argument.operation),
|
||||||
|
targetHandle:
|
||||||
|
model.schema!.operationByID.get(argument.argument)!.position_x >
|
||||||
|
model.schema!.operationByID.get(argument.operation)!.position_x
|
||||||
|
? 'right'
|
||||||
|
: 'left'
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}, [model.schema, setNodes, setEdges]);
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsModified(false);
|
||||||
|
}, PARAMETER.graphRefreshDelay);
|
||||||
|
}, [model.schema, setNodes, setEdges, setIsModified, toggleReset]);
|
||||||
|
|
||||||
const getPositions = useCallback(
|
const getPositions = useCallback(
|
||||||
() =>
|
() =>
|
||||||
|
@ -66,22 +94,46 @@ function OssFlow() {
|
||||||
|
|
||||||
const handleNodesChange = useCallback(
|
const handleNodesChange = useCallback(
|
||||||
(changes: NodeChange[]) => {
|
(changes: NodeChange[]) => {
|
||||||
|
if (changes.some(change => change.type === 'position' && change.position)) {
|
||||||
|
setIsModified(true);
|
||||||
|
}
|
||||||
onNodesChange(changes);
|
onNodesChange(changes);
|
||||||
},
|
},
|
||||||
[onNodesChange]
|
[onNodesChange, setIsModified]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEdgesChange = useCallback(
|
const handleSavePositions = useCallback(() => {
|
||||||
(changes: EdgeChange[]) => {
|
controller.savePositions(getPositions(), () => setIsModified(false));
|
||||||
onEdgesChange(changes);
|
}, [controller, getPositions, setIsModified]);
|
||||||
},
|
|
||||||
[onEdgesChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCreateOperation = useCallback(() => {
|
const handleCreateOperation = useCallback(() => {
|
||||||
// TODO: calculate insert location
|
const center = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||||
controller.promptCreateOperation(viewport.x, viewport.y, getPositions());
|
console.log(center);
|
||||||
}, [controller, viewport, getPositions]);
|
controller.promptCreateOperation(center.x, center.y, getPositions());
|
||||||
|
}, [controller, getPositions, flow]);
|
||||||
|
|
||||||
|
const handleDeleteOperation = useCallback(() => {
|
||||||
|
if (controller.selected.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.deleteOperation(controller.selected[0], getPositions());
|
||||||
|
}, [controller, getPositions]);
|
||||||
|
|
||||||
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
|
// Hotkeys implementation
|
||||||
|
if (controller.isProcessing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!controller.isMutable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'Delete') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
handleDeleteOperation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const proOptions: ProOptions = useMemo(() => ({ hideAttribution: true }), []);
|
const proOptions: ProOptions = useMemo(() => ({ hideAttribution: true }), []);
|
||||||
const canvasWidth = useMemo(() => 'calc(100vw - 1rem)', []);
|
const canvasWidth = useMemo(() => 'calc(100vw - 1rem)', []);
|
||||||
|
@ -101,21 +153,29 @@ function OssFlow() {
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onNodesChange={handleNodesChange}
|
onNodesChange={handleNodesChange}
|
||||||
onEdgesChange={handleEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
fitView
|
fitView
|
||||||
proOptions={proOptions}
|
proOptions={proOptions}
|
||||||
nodeTypes={OssNodeTypes}
|
nodeTypes={OssNodeTypes}
|
||||||
maxZoom={2}
|
maxZoom={2}
|
||||||
minZoom={0.75}
|
minZoom={0.75}
|
||||||
|
nodesConnectable={false}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[nodes, edges, proOptions, handleNodesChange, handleEdgesChange, OssNodeTypes]
|
[nodes, edges, proOptions, handleNodesChange, onEdgesChange, OssNodeTypes]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimateFade>
|
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||||
<Overlay position='top-0 pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
|
<Overlay position='top-0 pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
|
||||||
<ToolbarOssGraph onCreate={handleCreateOperation} />
|
<ToolbarOssGraph
|
||||||
|
isModified={isModified}
|
||||||
|
onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })}
|
||||||
|
onCreate={handleCreateOperation}
|
||||||
|
onDelete={handleDeleteOperation}
|
||||||
|
onResetPositions={() => setToggleReset(prev => !prev)}
|
||||||
|
onSavePositions={handleSavePositions}
|
||||||
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
<div className='relative' style={{ height: canvasHeight, width: canvasWidth }}>
|
<div className='relative' style={{ height: canvasHeight, width: canvasWidth }}>
|
||||||
{graph}
|
{graph}
|
||||||
|
|
|
@ -1,22 +1,56 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { IconNewItem } from '@/components/Icons';
|
import { IconDestroy, IconFitImage, IconNewItem, IconReset, IconSave } from '@/components/Icons';
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
import { prepareTooltip } from '@/utils/labels';
|
||||||
|
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
|
|
||||||
interface ToolbarOssGraphProps {
|
interface ToolbarOssGraphProps {
|
||||||
|
isModified: boolean;
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
onFitView: () => void;
|
||||||
|
onSavePositions: () => void;
|
||||||
|
onResetPositions: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarOssGraph({ onCreate }: ToolbarOssGraphProps) {
|
function ToolbarOssGraph({
|
||||||
|
isModified,
|
||||||
|
onCreate,
|
||||||
|
onDelete,
|
||||||
|
onFitView,
|
||||||
|
onSavePositions,
|
||||||
|
onResetPositions
|
||||||
|
}: ToolbarOssGraphProps) {
|
||||||
const controller = useOssEdit();
|
const controller = useOssEdit();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='cc-icons'>
|
<div className='cc-icons'>
|
||||||
|
{controller.isMutable ? (
|
||||||
|
<MiniButton
|
||||||
|
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||||
|
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||||
|
disabled={controller.isProcessing || !isModified}
|
||||||
|
onClick={onSavePositions}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{controller.isMutable ? (
|
||||||
|
<MiniButton
|
||||||
|
title='Сбросить изменения'
|
||||||
|
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||||
|
disabled={!isModified}
|
||||||
|
onClick={onResetPositions}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<MiniButton
|
||||||
|
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||||
|
title='Сбросить вид'
|
||||||
|
onClick={onFitView}
|
||||||
|
/>
|
||||||
{controller.isMutable ? (
|
{controller.isMutable ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Новая операция'
|
title='Новая операция'
|
||||||
|
@ -25,6 +59,14 @@ function ToolbarOssGraph({ onCreate }: ToolbarOssGraphProps) {
|
||||||
onClick={onCreate}
|
onClick={onCreate}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{controller.isMutable ? (
|
||||||
|
<MiniButton
|
||||||
|
title='Удалить выбранную'
|
||||||
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
|
disabled={controller.selected.length !== 1 || controller.isProcessing}
|
||||||
|
onClick={onDelete}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<BadgeHelp
|
<BadgeHelp
|
||||||
topic={HelpTopic.UI_OSS_GRAPH}
|
topic={HelpTopic.UI_OSS_GRAPH}
|
||||||
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
|
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
|
||||||
|
|
|
@ -13,12 +13,13 @@ import DlgCreateOperation from '@/dialogs/DlgCreateOperation';
|
||||||
import DlgEditEditors from '@/dialogs/DlgEditEditors';
|
import DlgEditEditors from '@/dialogs/DlgEditEditors';
|
||||||
import { AccessPolicy } from '@/models/library';
|
import { AccessPolicy } from '@/models/library';
|
||||||
import { Position2D } from '@/models/miscellaneous';
|
import { Position2D } from '@/models/miscellaneous';
|
||||||
import { IOperationCreateData, IOperationPosition, IOperationSchema } from '@/models/oss';
|
import { IOperationCreateData, IOperationPosition, IOperationSchema, OperationID } from '@/models/oss';
|
||||||
import { UserID, UserLevel } from '@/models/user';
|
import { UserID, UserLevel } from '@/models/user';
|
||||||
import { information } from '@/utils/labels';
|
import { information } from '@/utils/labels';
|
||||||
|
|
||||||
export interface IOssEditContext {
|
export interface IOssEditContext {
|
||||||
schema?: IOperationSchema;
|
schema?: IOperationSchema;
|
||||||
|
selected: OperationID[];
|
||||||
|
|
||||||
isMutable: boolean;
|
isMutable: boolean;
|
||||||
isProcessing: boolean;
|
isProcessing: boolean;
|
||||||
|
@ -29,9 +30,13 @@ export interface IOssEditContext {
|
||||||
promptLocation: () => void;
|
promptLocation: () => void;
|
||||||
toggleSubscribe: () => void;
|
toggleSubscribe: () => void;
|
||||||
|
|
||||||
|
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
||||||
|
|
||||||
share: () => void;
|
share: () => void;
|
||||||
|
|
||||||
|
savePositions: (positions: IOperationPosition[], callback?: () => void) => void;
|
||||||
promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void;
|
promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void;
|
||||||
|
deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OssEditContext = createContext<IOssEditContext | null>(null);
|
const OssEditContext = createContext<IOssEditContext | null>(null);
|
||||||
|
@ -45,10 +50,12 @@ export const useOssEdit = () => {
|
||||||
|
|
||||||
interface OssEditStateProps {
|
interface OssEditStateProps {
|
||||||
// isModified: boolean;
|
// isModified: boolean;
|
||||||
|
selected: OperationID[];
|
||||||
|
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OssEditState = ({ children }: OssEditStateProps) => {
|
export const OssEditState = ({ selected, setSelected, children }: OssEditStateProps) => {
|
||||||
// const router = useConceptNavigation();
|
// const router = useConceptNavigation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { adminMode } = useConceptOptions();
|
const { adminMode } = useConceptOptions();
|
||||||
|
@ -144,6 +151,23 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
|
||||||
[model]
|
[model]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const savePositions = useCallback(
|
||||||
|
(positions: IOperationPosition[], callback?: () => void) => {
|
||||||
|
model.savePositions({ positions: positions }, () => {
|
||||||
|
positions.forEach(item => {
|
||||||
|
const operation = model.schema?.operationByID.get(item.id);
|
||||||
|
if (operation) {
|
||||||
|
operation.position_x = item.position_x;
|
||||||
|
operation.position_y = item.position_y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
toast.success(information.changesSaved);
|
||||||
|
if (callback) callback();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[model]
|
||||||
|
);
|
||||||
|
|
||||||
const promptCreateOperation = useCallback((x: number, y: number, positions: IOperationPosition[]) => {
|
const promptCreateOperation = useCallback((x: number, y: number, positions: IOperationPosition[]) => {
|
||||||
setInsertPosition({ x: x, y: y });
|
setInsertPosition({ x: x, y: y });
|
||||||
setPositions(positions);
|
setPositions(positions);
|
||||||
|
@ -157,10 +181,21 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
|
||||||
[model]
|
[model]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const deleteOperation = useCallback(
|
||||||
|
(target: OperationID, positions: IOperationPosition[]) => {
|
||||||
|
model.deleteOperation({ target: target, positions: positions }, () =>
|
||||||
|
toast.success(information.operationDestroyed)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[model]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OssEditContext.Provider
|
<OssEditContext.Provider
|
||||||
value={{
|
value={{
|
||||||
schema: model.schema,
|
schema: model.schema,
|
||||||
|
selected,
|
||||||
|
|
||||||
isMutable,
|
isMutable,
|
||||||
isProcessing: model.processing,
|
isProcessing: model.processing,
|
||||||
|
|
||||||
|
@ -171,8 +206,11 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
|
||||||
promptLocation,
|
promptLocation,
|
||||||
|
|
||||||
share,
|
share,
|
||||||
|
setSelected,
|
||||||
|
|
||||||
promptCreateOperation
|
savePositions,
|
||||||
|
promptCreateOperation,
|
||||||
|
deleteOperation
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{model.schema ? (
|
{model.schema ? (
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
|
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useOSS } from '@/context/OssContext';
|
import { useOSS } from '@/context/OssContext';
|
||||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||||
|
import { OperationID } from '@/models/oss';
|
||||||
import { information, prompts } from '@/utils/labels';
|
import { information, prompts } from '@/utils/labels';
|
||||||
|
|
||||||
import EditorRSForm from './EditorOssCard';
|
import EditorRSForm from './EditorOssCard';
|
||||||
|
@ -39,6 +40,7 @@ function OssTabs() {
|
||||||
const { destroyItem } = useLibrary();
|
const { destroyItem } = useLibrary();
|
||||||
|
|
||||||
const [isModified, setIsModified] = useState(false);
|
const [isModified, setIsModified] = useState(false);
|
||||||
|
const [selected, setSelected] = useState<OperationID[]>([]);
|
||||||
useBlockNavigation(isModified);
|
useBlockNavigation(isModified);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
@ -112,14 +114,14 @@ function OssTabs() {
|
||||||
const graphPanel = useMemo(
|
const graphPanel = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<EditorTermGraph />
|
<EditorTermGraph isModified={isModified} setIsModified={setIsModified} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
),
|
),
|
||||||
[]
|
[isModified]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OssEditState>
|
<OssEditState selected={selected} setSelected={setSelected}>
|
||||||
{loading ? <Loader /> : null}
|
{loading ? <Loader /> : null}
|
||||||
{errorLoading ? <ProcessError error={errorLoading} /> : null}
|
{errorLoading ? <ProcessError error={errorLoading} /> : null}
|
||||||
{schema && !loading ? (
|
{schema && !loading ? (
|
||||||
|
|
|
@ -311,7 +311,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
showParamsDialog={() => setShowParamsDialog(true)}
|
showParamsDialog={() => setShowParamsDialog(true)}
|
||||||
onCreate={handleCreateCst}
|
onCreate={handleCreateCst}
|
||||||
onDelete={handleDeleteCst}
|
onDelete={handleDeleteCst}
|
||||||
onResetViewpoint={() => setToggleResetView(prev => !prev)}
|
onFitView={() => setToggleResetView(prev => !prev)}
|
||||||
onSaveImage={handleSaveImage}
|
onSaveImage={handleSaveImage}
|
||||||
toggleOrbit={() => setOrbit(prev => !prev)}
|
toggleOrbit={() => setOrbit(prev => !prev)}
|
||||||
toggleFoldDerived={handleFoldDerived}
|
toggleFoldDerived={handleFoldDerived}
|
||||||
|
|
|
@ -29,7 +29,7 @@ interface ToolbarTermGraphProps {
|
||||||
showParamsDialog: () => void;
|
showParamsDialog: () => void;
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onResetViewpoint: () => void;
|
onFitView: () => void;
|
||||||
onSaveImage: () => void;
|
onSaveImage: () => void;
|
||||||
|
|
||||||
toggleFoldDerived: () => void;
|
toggleFoldDerived: () => void;
|
||||||
|
@ -48,7 +48,7 @@ function ToolbarTermGraph({
|
||||||
showParamsDialog,
|
showParamsDialog,
|
||||||
onCreate,
|
onCreate,
|
||||||
onDelete,
|
onDelete,
|
||||||
onResetViewpoint,
|
onFitView,
|
||||||
onSaveImage
|
onSaveImage
|
||||||
}: ToolbarTermGraphProps) {
|
}: ToolbarTermGraphProps) {
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
@ -63,7 +63,7 @@ function ToolbarTermGraph({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||||
title='Граф целиком'
|
title='Граф целиком'
|
||||||
onClick={onResetViewpoint}
|
onClick={onFitView}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={!noText ? 'Скрыть текст' : 'Отобразить текст'}
|
title={!noText ? 'Скрыть текст' : 'Отобразить текст'}
|
||||||
|
|
|
@ -34,11 +34,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__node-input,
|
.react-flow__handle {
|
||||||
.react-flow__node-synthesis {
|
cursor: default !important;
|
||||||
|
|
||||||
|
border-color: var(--cl-bg-40);
|
||||||
|
background-color: var(--cl-bg-120);
|
||||||
|
|
||||||
|
.selected & {
|
||||||
|
border-color: var(--cd-bg-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
border-color: var(--cd-bg-40);
|
||||||
|
background-color: var(--cd-bg-120);
|
||||||
|
|
||||||
|
.selected & {
|
||||||
|
border-color: var(--cl-bg-40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__pane {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.react-flow__node-input, .react-flow__node-synthesis) {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
width: 120px;
|
width: 150px;
|
||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: var(--cl-bg-120);
|
background-color: var(--cl-bg-120);
|
||||||
|
@ -47,9 +72,25 @@
|
||||||
border-color: var(--cl-bg-40);
|
border-color: var(--cl-bg-40);
|
||||||
background-color: var(--cl-bg-120);
|
background-color: var(--cl-bg-120);
|
||||||
|
|
||||||
&.dark {
|
&:hover:not(.selected) {
|
||||||
|
box-shadow: 0 0 0 2px var(--cl-prim-bg-80) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: var(--cd-bg-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark & {
|
||||||
color: var(--cd-fg-100);
|
color: var(--cd-fg-100);
|
||||||
border-color: var(--cd-bg-40);
|
border-color: var(--cd-bg-40);
|
||||||
background-color: var(--cd-bg-120);
|
background-color: var(--cd-bg-120);
|
||||||
|
|
||||||
|
&:hover:not(.selected) {
|
||||||
|
box-shadow: 0 0 0 3px var(--cd-prim-bg-80) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: var(--cl-bg-40);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ export const PARAMETER = {
|
||||||
refreshTimeout: 100, // milliseconds delay for post-refresh actions
|
refreshTimeout: 100, // milliseconds delay for post-refresh actions
|
||||||
minimalTimeout: 10, // milliseconds delay for fast updates
|
minimalTimeout: 10, // milliseconds delay for fast updates
|
||||||
|
|
||||||
|
zoomDuration: 500, // milliseconds animation duration
|
||||||
|
|
||||||
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
|
||||||
graphPopupDelay: 500, // milliseconds delay for graph popup selections
|
graphPopupDelay: 500, // milliseconds delay for graph popup selections
|
||||||
|
|
|
@ -939,6 +939,7 @@ export const information = {
|
||||||
|
|
||||||
versionDestroyed: 'Версия удалена',
|
versionDestroyed: 'Версия удалена',
|
||||||
itemDestroyed: 'Схема удалена',
|
itemDestroyed: 'Схема удалена',
|
||||||
|
operationDestroyed: 'Операция удалена',
|
||||||
constituentsDestroyed: (aliases: string) => `Конституенты удалены: ${aliases}`
|
constituentsDestroyed: (aliases: string) => `Конституенты удалены: ${aliases}`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user