Improve OSS frontend
Some checks are pending
Backend CI / build (3.12) (push) Waiting to run
Frontend CI / build (22.x) (push) Waiting to run

This commit is contained in:
Ivan 2024-07-23 23:04:21 +03:00
parent 1bcf660c15
commit bf7902258b
16 changed files with 357 additions and 96 deletions

View File

@ -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'

View File

@ -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`,

View File

@ -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}

View File

@ -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[];
} }
/** /**

View File

@ -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>
); );
} }

View File

@ -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 = () => {
// controller.selectNode(id); console.log('edit operation ' + id);
// controller.showSelectInput(); //controller.selectNode(id);
//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>

View File

@ -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' }} />
</> </>
); );
} }

View File

@ -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}

View File

@ -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]')}

View File

@ -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 ? (

View File

@ -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 ? (

View File

@ -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}

View File

@ -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 ? 'Скрыть текст' : 'Отобразить текст'}

View File

@ -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);
}
} }
} }

View File

@ -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

View File

@ -939,6 +939,7 @@ export const information = {
versionDestroyed: 'Версия удалена', versionDestroyed: 'Версия удалена',
itemDestroyed: 'Схема удалена', itemDestroyed: 'Схема удалена',
operationDestroyed: 'Операция удалена',
constituentsDestroyed: (aliases: string) => `Конституенты удалены: ${aliases}` constituentsDestroyed: (aliases: string) => `Конституенты удалены: ${aliases}`
}; };