mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
This commit is contained in:
parent
acd24a28e5
commit
8376c6bda1
|
@ -43,6 +43,7 @@ This readme file is used mostly to document project dependencies
|
|||
- use-debounce
|
||||
- framer-motion
|
||||
- reagraph
|
||||
- html-to-image
|
||||
- @tanstack/react-table
|
||||
- @uiw/react-codemirror
|
||||
- @uiw/codemirror-themes
|
||||
|
|
7
rsconcept/frontend/package-lock.json
generated
7
rsconcept/frontend/package-lock.json
generated
|
@ -15,6 +15,7 @@
|
|||
"axios": "^1.7.2",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.3.8",
|
||||
"html-to-image": "^1.11.11",
|
||||
"js-file-download": "^0.4.12",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
@ -6808,6 +6809,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-to-image": {
|
||||
"version": "1.11.11",
|
||||
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz",
|
||||
"integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"axios": "^1.7.2",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.3.8",
|
||||
"html-to-image": "^1.11.11",
|
||||
"js-file-download": "^0.4.12",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
|
|
@ -38,6 +38,7 @@ export { LuFolderClosed as IconFolderClosed } from 'react-icons/lu';
|
|||
export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu';
|
||||
export { LuLightbulb as IconHelp } from 'react-icons/lu';
|
||||
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
|
||||
export { TbGridDots as IconGrid } from 'react-icons/tb';
|
||||
export { RiPushpinFill as IconPin } from 'react-icons/ri';
|
||||
export { RiUnpinLine as IconUnpin } from 'react-icons/ri';
|
||||
export { BiCaretDown as IconSortDesc } from 'react-icons/bi';
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { storage } from '@/utils/constants';
|
||||
|
||||
import OssFlow from './OssFlow';
|
||||
|
||||
interface EditorOssGraphProps {
|
||||
|
@ -10,9 +13,11 @@ interface EditorOssGraphProps {
|
|||
}
|
||||
|
||||
function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) {
|
||||
const [showGrid, setShowGrid] = useLocalStorage<boolean>(storage.ossShowGrid, false);
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<OssFlow isModified={isModified} setIsModified={setIsModified} />
|
||||
<OssFlow isModified={isModified} setIsModified={setIsModified} showGrid={showGrid} setShowGrid={setShowGrid} />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
import { IconDestroy, IconEdit2 } from '@/components/Icons';
|
||||
import { IconRSForm } from '@/components/Icons';
|
||||
import MiniButton from '@/components/ui/MiniButton.tsx';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { useOSS } from '@/context/OssContext';
|
||||
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
|
||||
|
@ -14,40 +16,30 @@ interface InputNodeProps {
|
|||
|
||||
function InputNode({ id, data }: InputNodeProps) {
|
||||
const controller = useOssEdit();
|
||||
console.log(controller.isMutable);
|
||||
const model = useOSS();
|
||||
|
||||
const handleDelete = () => {
|
||||
console.log('delete node ' + id);
|
||||
};
|
||||
const hasFile = !!model.schema?.operationByID.get(Number(id))?.result;
|
||||
|
||||
const handleEditOperation = () => {
|
||||
console.log('edit operation ' + id);
|
||||
//controller.selectNode(id);
|
||||
//controller.showSynthesis();
|
||||
const handleOpenSchema = () => {
|
||||
controller.openOperationSchema(Number(id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle type='source' position={Position.Bottom} />
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex-grow text-center'>{data.label}</div>
|
||||
<div className='cc-icons'>
|
||||
|
||||
<Overlay position='top-[-0.2rem] right-[-0.2rem]' className='cc-icons'>
|
||||
<MiniButton
|
||||
icon={<IconEdit2 className='icon-primary' size='0.75rem' />}
|
||||
noPadding
|
||||
title='Редактировать'
|
||||
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />}
|
||||
noHover
|
||||
title='Связанная КС'
|
||||
onClick={() => {
|
||||
handleEditOperation();
|
||||
handleOpenSchema();
|
||||
}}
|
||||
disabled={!hasFile}
|
||||
/>
|
||||
<MiniButton
|
||||
noPadding
|
||||
icon={<IconDestroy className='icon-red' size='0.75rem' />}
|
||||
title='Удалить операцию'
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
<div className='flex-grow text-center text-sm'>{data.label}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { VscDebugStart } from 'react-icons/vsc';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
import { IconDestroy, IconEdit2 } from '@/components/Icons';
|
||||
import { IconRSForm } from '@/components/Icons';
|
||||
import MiniButton from '@/components/ui/MiniButton.tsx';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { useOSS } from '@/context/OssContext';
|
||||
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
interface OperationNodeProps {
|
||||
|
@ -14,50 +15,30 @@ interface OperationNodeProps {
|
|||
|
||||
function OperationNode({ id, data }: OperationNodeProps) {
|
||||
const controller = useOssEdit();
|
||||
console.log(controller.isMutable);
|
||||
const model = useOSS();
|
||||
|
||||
const handleDelete = () => {
|
||||
console.log('delete node ' + id);
|
||||
// onDelete(id);
|
||||
};
|
||||
const hasFile = !!model.schema?.operationByID.get(Number(id))?.result;
|
||||
|
||||
const handleEditOperation = () => {
|
||||
console.log('edit operation ' + id);
|
||||
//controller.selectNode(id);
|
||||
//controller.showSynthesis();
|
||||
};
|
||||
|
||||
const handleRunOperation = () => {
|
||||
console.log('run operation');
|
||||
// controller.singleSynthesis(id);
|
||||
const handleOpenSchema = () => {
|
||||
controller.openOperationSchema(Number(id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle type='source' position={Position.Bottom} />
|
||||
<div className='flex justify-between'>
|
||||
<div className='flex-grow text-center'>{data.label}</div>
|
||||
<div className='cc-icons'>
|
||||
|
||||
<Overlay position='top-[-0.2rem] right-[-0.2rem]' className='cc-icons'>
|
||||
<MiniButton
|
||||
icon={<IconEdit2 className='icon-primary' size='1rem' />}
|
||||
title='Редактировать'
|
||||
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />}
|
||||
noHover
|
||||
title='Связанная КС'
|
||||
onClick={() => {
|
||||
handleEditOperation();
|
||||
handleOpenSchema();
|
||||
}}
|
||||
disabled={!hasFile}
|
||||
/>
|
||||
<MiniButton
|
||||
className='float-right'
|
||||
icon={<VscDebugStart className='icon-green' size='1rem' />}
|
||||
title='Синтез'
|
||||
onClick={() => handleRunOperation()}
|
||||
/>
|
||||
<MiniButton
|
||||
icon={<IconDestroy className='icon-red' size='1rem' />}
|
||||
title='Удалить операцию'
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
<div className='flex-grow text-center text-sm'>{data.label}</div>
|
||||
|
||||
<Handle type='target' position={Position.Top} id='left' style={{ left: 40 }} />
|
||||
<Handle type='target' position={Position.Top} id='right' style={{ right: 40, left: 'auto' }} />
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { toSvg } from 'html-to-image';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
Background,
|
||||
getNodesBounds,
|
||||
getViewportForBounds,
|
||||
Node,
|
||||
NodeChange,
|
||||
NodeTypes,
|
||||
|
@ -18,6 +23,7 @@ import AnimateFade from '@/components/wrap/AnimateFade';
|
|||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useOSS } from '@/context/OssContext';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { errors } from '@/utils/labels';
|
||||
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
import InputNode from './InputNode';
|
||||
|
@ -27,10 +33,12 @@ import ToolbarOssGraph from './ToolbarOssGraph';
|
|||
interface OssFlowProps {
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
showGrid: boolean;
|
||||
setShowGrid: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||
const { calculateHeight } = useConceptOptions();
|
||||
function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowProps) {
|
||||
const { calculateHeight, colors } = useConceptOptions();
|
||||
const model = useOSS();
|
||||
const controller = useOssEdit();
|
||||
const flow = useReactFlow();
|
||||
|
@ -119,6 +127,47 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
controller.deleteOperation(controller.selected[0], getPositions());
|
||||
}, [controller, getPositions]);
|
||||
|
||||
const handleFitView = useCallback(() => {
|
||||
flow.fitView({ duration: PARAMETER.zoomDuration });
|
||||
}, [flow]);
|
||||
|
||||
const handleResetPositions = useCallback(() => {
|
||||
setToggleReset(prev => !prev);
|
||||
}, []);
|
||||
|
||||
const handleSaveImage = useCallback(() => {
|
||||
const canvas: HTMLElement | null = document.querySelector('.react-flow__viewport');
|
||||
if (canvas === null) {
|
||||
toast.error(errors.imageFailed);
|
||||
return;
|
||||
}
|
||||
|
||||
const imageWidth = PARAMETER.ossImageWidth;
|
||||
const imageHeight = PARAMETER.ossImageHeight;
|
||||
const nodesBounds = getNodesBounds(nodes);
|
||||
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, 0.5, 2);
|
||||
toSvg(canvas, {
|
||||
backgroundColor: colors.bgDefault,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
style: {
|
||||
width: String(imageWidth),
|
||||
height: String(imageHeight),
|
||||
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`
|
||||
}
|
||||
})
|
||||
.then(dataURL => {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('download', 'reactflow.svg');
|
||||
a.setAttribute('href', dataURL);
|
||||
a.click();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
toast.error(errors.imageFailed);
|
||||
});
|
||||
}, [colors, nodes]);
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
// Hotkeys implementation
|
||||
if (controller.isProcessing) {
|
||||
|
@ -127,6 +176,12 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
if (!controller.isMutable) {
|
||||
return;
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleSavePositions();
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Delete') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -160,9 +215,13 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
maxZoom={2}
|
||||
minZoom={0.75}
|
||||
nodesConnectable={false}
|
||||
/>
|
||||
snapToGrid={true}
|
||||
snapGrid={[10, 10]}
|
||||
>
|
||||
{showGrid ? <Background gap={10} /> : null}
|
||||
</ReactFlow>
|
||||
),
|
||||
[nodes, edges, proOptions, handleNodesChange, onEdgesChange, OssNodeTypes]
|
||||
[nodes, edges, proOptions, handleNodesChange, onEdgesChange, OssNodeTypes, showGrid]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -170,11 +229,14 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
<Overlay position='top-0 pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
|
||||
<ToolbarOssGraph
|
||||
isModified={isModified}
|
||||
onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })}
|
||||
showGrid={showGrid}
|
||||
onFitView={handleFitView}
|
||||
onCreate={handleCreateOperation}
|
||||
onDelete={handleDeleteOperation}
|
||||
onResetPositions={() => setToggleReset(prev => !prev)}
|
||||
onResetPositions={handleResetPositions}
|
||||
onSavePositions={handleSavePositions}
|
||||
onSaveImage={handleSaveImage}
|
||||
toggleShowGrid={() => setShowGrid(prev => !prev)}
|
||||
/>
|
||||
</Overlay>
|
||||
<div className='relative' style={{ height: canvasHeight, width: canvasWidth }}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { IconDestroy, IconFitImage, IconNewItem, IconReset, IconSave } from '@/components/Icons';
|
||||
import { IconDestroy, IconFitImage, IconGrid, IconImage, IconNewItem, IconReset, IconSave } from '@/components/Icons';
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
@ -11,20 +11,26 @@ import { useOssEdit } from '../OssEditContext';
|
|||
|
||||
interface ToolbarOssGraphProps {
|
||||
isModified: boolean;
|
||||
showGrid: boolean;
|
||||
onCreate: () => void;
|
||||
onDelete: () => void;
|
||||
onFitView: () => void;
|
||||
onSaveImage: () => void;
|
||||
onSavePositions: () => void;
|
||||
onResetPositions: () => void;
|
||||
toggleShowGrid: () => void;
|
||||
}
|
||||
|
||||
function ToolbarOssGraph({
|
||||
isModified,
|
||||
showGrid,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onFitView,
|
||||
onSaveImage,
|
||||
onSavePositions,
|
||||
onResetPositions
|
||||
onResetPositions,
|
||||
toggleShowGrid
|
||||
}: ToolbarOssGraphProps) {
|
||||
const controller = useOssEdit();
|
||||
|
||||
|
@ -51,6 +57,17 @@ function ToolbarOssGraph({
|
|||
title='Сбросить вид'
|
||||
onClick={onFitView}
|
||||
/>
|
||||
<MiniButton
|
||||
title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'}
|
||||
icon={
|
||||
showGrid ? (
|
||||
<IconGrid size='1.25rem' className='icon-green' />
|
||||
) : (
|
||||
<IconGrid size='1.25rem' className='icon-primary' />
|
||||
)
|
||||
}
|
||||
onClick={toggleShowGrid}
|
||||
/>
|
||||
{controller.isMutable ? (
|
||||
<MiniButton
|
||||
title='Новая операция'
|
||||
|
@ -67,6 +84,11 @@ function ToolbarOssGraph({
|
|||
onClick={onDelete}
|
||||
/>
|
||||
) : null}
|
||||
<MiniButton
|
||||
icon={<IconImage size='1.25rem' className='icon-primary' />}
|
||||
title='Сохранить изображение'
|
||||
onClick={onSaveImage}
|
||||
/>
|
||||
<BadgeHelp
|
||||
topic={HelpTopic.UI_OSS_GRAPH}
|
||||
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
|
||||
|
|
|
@ -4,9 +4,11 @@ import { AnimatePresence } from 'framer-motion';
|
|||
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { useOSS } from '@/context/OssContext';
|
||||
import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
|
||||
import DlgCreateOperation from '@/dialogs/DlgCreateOperation';
|
||||
|
@ -34,6 +36,8 @@ export interface IOssEditContext {
|
|||
|
||||
share: () => void;
|
||||
|
||||
openOperationSchema: (target: OperationID) => void;
|
||||
|
||||
savePositions: (positions: IOperationPosition[], callback?: () => void) => void;
|
||||
promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void;
|
||||
deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||
|
@ -56,7 +60,7 @@ interface OssEditStateProps {
|
|||
}
|
||||
|
||||
export const OssEditState = ({ selected, setSelected, children }: OssEditStateProps) => {
|
||||
// const router = useConceptNavigation();
|
||||
const router = useConceptNavigation();
|
||||
const { user } = useAuth();
|
||||
const { adminMode } = useConceptOptions();
|
||||
const { accessLevel, setAccessLevel } = useAccessMode();
|
||||
|
@ -151,6 +155,17 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
[model]
|
||||
);
|
||||
|
||||
const openOperationSchema = useCallback(
|
||||
(target: OperationID) => {
|
||||
const node = model.schema?.operationByID.get(target);
|
||||
if (!node || !node.result) {
|
||||
return;
|
||||
}
|
||||
router.push(urls.schema(node.result));
|
||||
},
|
||||
[router, model]
|
||||
);
|
||||
|
||||
const savePositions = useCallback(
|
||||
(positions: IOperationPosition[], callback?: () => void) => {
|
||||
model.savePositions({ positions: positions }, () => {
|
||||
|
@ -208,6 +223,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
share,
|
||||
setSelected,
|
||||
|
||||
openOperationSchema,
|
||||
savePositions,
|
||||
promptCreateOperation,
|
||||
deleteOperation
|
||||
|
|
|
@ -12,6 +12,8 @@ export const PARAMETER = {
|
|||
minimalTimeout: 10, // milliseconds delay for fast updates
|
||||
|
||||
zoomDuration: 500, // milliseconds animation duration
|
||||
ossImageWidth: 1280, // pixels - size of OSS image
|
||||
ossImageHeight: 960, // pixels - size of OSS image
|
||||
|
||||
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
|
||||
|
@ -110,6 +112,8 @@ export const storage = {
|
|||
rsgraphSizing: 'rsgraph.sizing',
|
||||
rsgraphFoldHidden: 'rsgraph.fold_hidden',
|
||||
|
||||
ossShowGrid: 'oss.show_grid',
|
||||
|
||||
cstFilterMatch: 'cst.filter.match',
|
||||
cstFilterGraph: 'cst.filter.graph'
|
||||
};
|
||||
|
|
|
@ -948,7 +948,8 @@ export const information = {
|
|||
*/
|
||||
export const errors = {
|
||||
astFailed: 'Невозможно построить дерево разбора',
|
||||
passwordsMismatch: 'Пароли не совпадают'
|
||||
passwordsMismatch: 'Пароли не совпадают',
|
||||
imageFailed: 'Ошибка при создании изображения'
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue
Block a user