mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-11-20 17:21:24 +03:00
R: Extract action handlers
This commit is contained in:
parent
86ac5e5508
commit
c9bc3401b0
|
|
@ -116,7 +116,8 @@ class CstUpdateSerializer(StrictSerializer):
|
|||
raise serializers.ValidationError({
|
||||
'alias': msg.aliasTaken(new_alias)
|
||||
})
|
||||
if 'definition_formal' in attrs['item_data'] and cst.definition_formal != attrs['item_data']['definition_formal']:
|
||||
if 'definition_formal' in attrs['item_data'] \
|
||||
and cst.definition_formal != attrs['item_data']['definition_formal']:
|
||||
if Inheritance.objects.filter(child=cst).exists():
|
||||
raise serializers.ValidationError({
|
||||
'definition_formal': msg.changeInheritedDefinition()
|
||||
|
|
|
|||
|
|
@ -3,21 +3,14 @@
|
|||
import { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { DiagramFlow, useReactFlow, useStoreApi } from '@/components/flow/diagram-flow';
|
||||
import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
|
||||
import { useMainHeight } from '@/stores/app-layout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { promptText } from '@/utils/labels';
|
||||
import { withPreventDefault } from '@/utils/utils';
|
||||
|
||||
import { OperationType } from '../../../backend/types';
|
||||
import { useDeleteBlock } from '../../../backend/use-delete-block';
|
||||
import { useMutatingOss } from '../../../backend/use-mutating-oss';
|
||||
import { useUpdateLayout } from '../../../backend/use-update-layout';
|
||||
import { type IOssItem, NodeType } from '../../../models/oss';
|
||||
import { type OssNode, type Position2D } from '../../../models/oss-layout';
|
||||
import { GRID_SIZE, LayoutManager } from '../../../models/oss-layout-api';
|
||||
import { GRID_SIZE } from '../../../models/oss-layout-api';
|
||||
import { useOSSGraphStore } from '../../../stores/oss-graph';
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
||||
|
|
@ -30,6 +23,7 @@ import { SidePanel } from './side-panel';
|
|||
import { ToolbarOssGraph } from './toolbar-oss-graph';
|
||||
import { useDragging } from './use-dragging';
|
||||
import { useGetLayout } from './use-get-layout';
|
||||
import { useHandleActions } from './use-handle-actions';
|
||||
|
||||
export const flowOptions = {
|
||||
fitView: true,
|
||||
|
|
@ -46,158 +40,22 @@ export const flowOptions = {
|
|||
|
||||
export function OssFlow() {
|
||||
const mainHeight = useMainHeight();
|
||||
const {
|
||||
navigateOperationSchema,
|
||||
schema,
|
||||
selected,
|
||||
setSelected,
|
||||
selectedItems,
|
||||
isMutable,
|
||||
deselectAll,
|
||||
canDeleteOperation
|
||||
} = useOssEdit();
|
||||
const { navigateOperationSchema, schema } = useOssEdit();
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const { containMovement, nodes, onNodesChange, edges, onEdgesChange, resetGraph, resetView } = useOssFlow();
|
||||
const store = useStoreApi();
|
||||
const { resetSelectedElements } = store.getState();
|
||||
|
||||
const isProcessing = useMutatingOss();
|
||||
const { containMovement, nodes, onNodesChange, edges, onEdgesChange } = useOssFlow();
|
||||
|
||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||
const showPanel = usePreferencesStore(state => state.showOssSidePanel);
|
||||
|
||||
const getLayout = useGetLayout();
|
||||
const { updateLayout } = useUpdateLayout();
|
||||
const { deleteBlock } = useDeleteBlock();
|
||||
|
||||
const [mouseCoords, setMouseCoords] = useState<Position2D>({ x: 0, y: 0 });
|
||||
|
||||
const showCreateOperation = useDialogsStore(state => state.showCreateSynthesis);
|
||||
const showCreateBlock = useDialogsStore(state => state.showCreateBlock);
|
||||
const showCreateSchema = useDialogsStore(state => state.showCreateSchema);
|
||||
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||
const showDeleteReference = useDialogsStore(state => state.showDeleteReference);
|
||||
const showEditBlock = useDialogsStore(state => state.showEditBlock);
|
||||
const showImportSchema = useDialogsStore(state => state.showImportSchema);
|
||||
|
||||
const { isOpen: isContextMenuOpen, menuProps, openContextMenu, hideContextMenu } = useContextMenu();
|
||||
const { handleDragStart, handleDrag, handleDragStop } = useDragging({ hideContextMenu });
|
||||
|
||||
function handleSavePositions() {
|
||||
void updateLayout({ itemID: schema.id, data: getLayout() });
|
||||
}
|
||||
|
||||
function handleCreateSynthesis() {
|
||||
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
showCreateOperation({
|
||||
ossID: schema.id,
|
||||
layout: getLayout(),
|
||||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
initialInputs: selectedItems.filter(item => item?.nodeType === NodeType.OPERATION).map(item => item.id),
|
||||
initialParent: extractBlockParent(selectedItems),
|
||||
onCreate: newID => {
|
||||
resetView();
|
||||
setTimeout(() => setSelected([`o${newID}`]), PARAMETER.minimalTimeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateBlock() {
|
||||
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
const parent = extractBlockParent(selectedItems);
|
||||
const needChildren = parent === null || selectedItems.length !== 1 || parent !== selectedItems[0].id;
|
||||
showCreateBlock({
|
||||
ossID: schema.id,
|
||||
layout: getLayout(),
|
||||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
childrenBlocks: !needChildren
|
||||
? []
|
||||
: selectedItems.filter(item => item.nodeType === NodeType.BLOCK).map(item => item.id),
|
||||
childrenOperations: !needChildren
|
||||
? []
|
||||
: selectedItems.filter(item => item.nodeType === NodeType.OPERATION).map(item => item.id),
|
||||
initialParent: parent,
|
||||
onCreate: newID => {
|
||||
resetView();
|
||||
setTimeout(() => setSelected([`b${newID}`]), PARAMETER.minimalTimeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateSchema() {
|
||||
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
showCreateSchema({
|
||||
ossID: schema.id,
|
||||
layout: getLayout(),
|
||||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
initialParent: extractBlockParent(selectedItems),
|
||||
onCreate: newID => {
|
||||
resetView();
|
||||
setTimeout(() => setSelected([`o${newID}`]), PARAMETER.minimalTimeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleImportSchema() {
|
||||
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
showImportSchema({
|
||||
ossID: schema.id,
|
||||
layout: getLayout(),
|
||||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
initialParent: extractBlockParent(selectedItems),
|
||||
onCreate: newID => {
|
||||
resetView();
|
||||
setTimeout(() => setSelected([`o${newID}`]), PARAMETER.minimalTimeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleDeleteSelected() {
|
||||
if (selected.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const item = schema.itemByNodeID.get(selected[0]);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (item.nodeType === NodeType.OPERATION) {
|
||||
if (!canDeleteOperation(item)) {
|
||||
return;
|
||||
}
|
||||
switch (item.operation_type) {
|
||||
case OperationType.REPLICA:
|
||||
showDeleteReference({
|
||||
ossID: schema.id,
|
||||
targetID: item.id,
|
||||
layout: getLayout(),
|
||||
beforeDelete: deselectAll
|
||||
});
|
||||
break;
|
||||
case OperationType.INPUT:
|
||||
case OperationType.SYNTHESIS:
|
||||
showDeleteOperation({
|
||||
ossID: schema.id,
|
||||
targetID: item.id,
|
||||
layout: getLayout(),
|
||||
beforeDelete: deselectAll
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!window.confirm(promptText.deleteBlock)) {
|
||||
return;
|
||||
}
|
||||
void deleteBlock({
|
||||
itemID: schema.id,
|
||||
data: { target: item.id, layout: getLayout() },
|
||||
beforeUpdate: deselectAll
|
||||
});
|
||||
}
|
||||
}
|
||||
const { handleKeyDown } = useHandleActions();
|
||||
|
||||
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) {
|
||||
event.preventDefault();
|
||||
|
|
@ -219,109 +77,6 @@ export function OssFlow() {
|
|||
}
|
||||
}
|
||||
|
||||
function handleSelectLeft() {
|
||||
const selectedOperation = selectedItems.find(item => item.nodeType === NodeType.OPERATION);
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
const manager = new LayoutManager(schema, getLayout());
|
||||
const newNodeID = manager.selectLeft(selectedOperation.nodeID);
|
||||
if (newNodeID) {
|
||||
setSelected([newNodeID]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectRight() {
|
||||
const selectedOperation = selectedItems.find(item => item.nodeType === NodeType.OPERATION);
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
const manager = new LayoutManager(schema, getLayout());
|
||||
const newNodeID = manager.selectRight(selectedOperation.nodeID);
|
||||
if (newNodeID) {
|
||||
setSelected([newNodeID]);
|
||||
}
|
||||
}
|
||||
function handleSelectUp() {
|
||||
const selectedOperation = selectedItems.find(item => item.nodeType === NodeType.OPERATION);
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
const manager = new LayoutManager(schema, getLayout());
|
||||
const newNodeID = manager.selectUp(selectedOperation.nodeID);
|
||||
if (newNodeID) {
|
||||
setSelected([newNodeID]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectDown() {
|
||||
const selectedOperation = selectedItems.find(item => item.nodeType === NodeType.OPERATION);
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
const manager = new LayoutManager(schema, getLayout());
|
||||
const newNodeID = manager.selectDown(selectedOperation.nodeID);
|
||||
if (newNodeID) {
|
||||
setSelected([newNodeID]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
withPreventDefault(resetSelectedElements)(event);
|
||||
return;
|
||||
}
|
||||
if (!isMutable) {
|
||||
return;
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
||||
withPreventDefault(handleSavePositions)(event);
|
||||
return;
|
||||
}
|
||||
if (event.altKey) {
|
||||
if (event.code === 'Digit1') {
|
||||
withPreventDefault(handleCreateBlock)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Digit2') {
|
||||
withPreventDefault(handleCreateSynthesis)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Digit3') {
|
||||
withPreventDefault(handleImportSchema)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Digit4') {
|
||||
withPreventDefault(handleCreateSynthesis)(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event.code === 'Delete') {
|
||||
withPreventDefault(handleDeleteSelected)(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.code === 'ArrowLeft') {
|
||||
withPreventDefault(handleSelectLeft)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'ArrowRight') {
|
||||
withPreventDefault(handleSelectRight)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'ArrowUp') {
|
||||
withPreventDefault(handleSelectUp)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'ArrowDown') {
|
||||
withPreventDefault(handleSelectDown)(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseMove(event: React.MouseEvent<HTMLDivElement>) {
|
||||
const targetPosition = screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
||||
setMouseCoords(targetPosition);
|
||||
|
|
@ -346,12 +101,6 @@ export function OssFlow() {
|
|||
|
||||
<ToolbarOssGraph
|
||||
className='cc-tab-tools'
|
||||
onCreateBlock={handleCreateBlock}
|
||||
onCreateSchema={handleCreateSchema}
|
||||
onImportSchema={handleImportSchema}
|
||||
onCreateSynthesis={handleCreateSynthesis}
|
||||
onDelete={handleDeleteSelected}
|
||||
onResetPositions={resetGraph}
|
||||
openContextMenu={openContextMenu}
|
||||
isContextMenuOpen={isContextMenuOpen}
|
||||
hideContextMenu={hideContextMenu}
|
||||
|
|
@ -387,12 +136,3 @@ export function OssFlow() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// -------- Internals --------
|
||||
function extractBlockParent(selectedItems: IOssItem[]): number | null {
|
||||
if (selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK) {
|
||||
return selectedItems[0].id;
|
||||
}
|
||||
const parents = selectedItems.map(item => item.parent).filter(id => id !== null);
|
||||
return parents.length === 0 ? null : parents[0];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,54 +26,45 @@ import {
|
|||
} from '@/components/icons';
|
||||
import { type Styling } from '@/components/props';
|
||||
import { cn } from '@/components/utils';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { isIOS, isMac, notImplemented, prepareTooltip } from '@/utils/utils';
|
||||
|
||||
import { useMutatingOss } from '../../../backend/use-mutating-oss';
|
||||
import { useUpdateLayout } from '../../../backend/use-update-layout';
|
||||
import { NodeType } from '../../../models/oss';
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
||||
import { useOssFlow } from './oss-flow-context';
|
||||
import { useGetLayout } from './use-get-layout';
|
||||
import { useHandleActions } from './use-handle-actions';
|
||||
|
||||
interface ToolbarOssGraphProps extends Styling {
|
||||
onCreateBlock: () => void;
|
||||
onCreateSchema: () => void;
|
||||
onImportSchema: () => void;
|
||||
onCreateSynthesis: () => void;
|
||||
onDelete: () => void;
|
||||
onResetPositions: () => void;
|
||||
|
||||
isContextMenuOpen: boolean;
|
||||
openContextMenu: (node: OssNode, clientX: number, clientY: number) => void;
|
||||
hideContextMenu: () => void;
|
||||
}
|
||||
|
||||
export function ToolbarOssGraph({
|
||||
onCreateBlock,
|
||||
onCreateSchema,
|
||||
onImportSchema,
|
||||
onCreateSynthesis,
|
||||
onDelete,
|
||||
onResetPositions,
|
||||
|
||||
isContextMenuOpen,
|
||||
openContextMenu,
|
||||
hideContextMenu,
|
||||
className,
|
||||
...restProps
|
||||
}: ToolbarOssGraphProps) {
|
||||
const { schema, selectedItems, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||
const { selectedItems, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||
const isProcessing = useMutatingOss();
|
||||
const { resetView, nodes } = useOssFlow();
|
||||
const getLayout = useGetLayout();
|
||||
const { updateLayout } = useUpdateLayout();
|
||||
const { user } = useAuthSuspense();
|
||||
const { elementRef: menuRef, isOpen: isMenuOpen, toggle: toggleMenu, handleBlur: handleMenuBlur } = useDropdown();
|
||||
const {
|
||||
handleSavePositions,
|
||||
handleCreateSynthesis,
|
||||
handleCreateBlock,
|
||||
handleCreateSchema,
|
||||
handleImportSchema,
|
||||
handleDeleteSelected,
|
||||
handleResetPositions,
|
||||
handleShowOptions
|
||||
} = useHandleActions();
|
||||
|
||||
const showOptions = useDialogsStore(state => state.showOssOptions);
|
||||
const showSidePanel = usePreferencesStore(state => state.showOssSidePanel);
|
||||
const toggleShowSidePanel = usePreferencesStore(state => state.toggleShowOssSidePanel);
|
||||
|
||||
|
|
@ -87,14 +78,6 @@ export function ToolbarOssGraph({
|
|||
toggleMenu();
|
||||
}
|
||||
|
||||
function handleShowOptions() {
|
||||
showOptions();
|
||||
}
|
||||
|
||||
function handleSavePositions() {
|
||||
void updateLayout({ itemID: schema.id, data: getLayout() });
|
||||
}
|
||||
|
||||
function handleEditItem(event: React.MouseEvent<HTMLButtonElement>) {
|
||||
if (isContextMenuOpen) {
|
||||
hideContextMenu();
|
||||
|
|
@ -123,7 +106,7 @@ export function ToolbarOssGraph({
|
|||
<MiniButton
|
||||
title='Сбросить изменения'
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
onClick={onResetPositions}
|
||||
onClick={handleResetPositions}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Сбросить вид'
|
||||
|
|
@ -173,25 +156,25 @@ export function ToolbarOssGraph({
|
|||
text='Новый блок'
|
||||
titleHtml={prepareTooltip('Новый блок', 'Alt + 1')}
|
||||
icon={<IconConceptBlock size='1.25rem' className='text-constructive' />}
|
||||
onClick={onCreateBlock}
|
||||
onClick={handleCreateBlock}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='Новая КС'
|
||||
titleHtml={prepareTooltip('Новая концептуальная схема', 'Alt + 2')}
|
||||
icon={<IconNewItem size='1.25rem' className='text-constructive' />}
|
||||
onClick={onCreateSchema}
|
||||
onClick={handleCreateSchema}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='Импорт КС'
|
||||
titleHtml={prepareTooltip('Импорт концептуальной схемы', 'Alt + 3')}
|
||||
icon={<IconDownload size='1.25rem' className='text-primary' />}
|
||||
onClick={onImportSchema}
|
||||
onClick={handleImportSchema}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='Синтез'
|
||||
titleHtml={prepareTooltip('Синтез концептуальных схем', 'Alt + 4')}
|
||||
icon={<IconSynthesis size='1.25rem' className='text-primary' />}
|
||||
onClick={onCreateSynthesis}
|
||||
onClick={handleCreateSynthesis}
|
||||
/>
|
||||
{user.is_staff ? (
|
||||
<DropdownButton
|
||||
|
|
@ -218,7 +201,7 @@ export function ToolbarOssGraph({
|
|||
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
||||
hideTitle={isMenuOpen}
|
||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||
onClick={onDelete}
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={
|
||||
isProcessing ||
|
||||
(!selectedOperation && !selectedBlock) ||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,286 @@
|
|||
import { useReactFlow, useStoreApi } from 'reactflow';
|
||||
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { promptText } from '@/utils/labels';
|
||||
import { withPreventDefault } from '@/utils/utils';
|
||||
|
||||
import { OperationType } from '../../../backend/types';
|
||||
import { useDeleteBlock } from '../../../backend/use-delete-block';
|
||||
import { useMutatingOss } from '../../../backend/use-mutating-oss';
|
||||
import { useUpdateLayout } from '../../../backend/use-update-layout';
|
||||
import { type IOssItem, NodeType } from '../../../models/oss';
|
||||
import { LayoutManager } from '../../../models/oss-layout-api';
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
||||
import { useOssFlow } from './oss-flow-context';
|
||||
import { useGetLayout } from './use-get-layout';
|
||||
|
||||
export function useHandleActions() {
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const { schema, selected, setSelected, selectedItems, isMutable, deselectAll, canDeleteOperation } = useOssEdit();
|
||||
const { resetView, resetGraph } = useOssFlow();
|
||||
const isProcessing = useMutatingOss();
|
||||
const store = useStoreApi();
|
||||
const { resetSelectedElements } = store.getState();
|
||||
|
||||
const getLayout = useGetLayout();
|
||||
const { updateLayout } = useUpdateLayout();
|
||||
const { deleteBlock } = useDeleteBlock();
|
||||
|
||||
const showCreateOperation = useDialogsStore(state => state.showCreateSynthesis);
|
||||
const showCreateBlock = useDialogsStore(state => state.showCreateBlock);
|
||||
const showCreateSchema = useDialogsStore(state => state.showCreateSchema);
|
||||
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||
const showDeleteReference = useDialogsStore(state => state.showDeleteReference);
|
||||
const showImportSchema = useDialogsStore(state => state.showImportSchema);
|
||||
const showOptions = useDialogsStore(state => state.showOssOptions);
|
||||
|
||||
function handleShowOptions() {
|
||||
showOptions();
|
||||
}
|
||||
|
||||
function handleSavePositions() {
|
||||
void updateLayout({ itemID: schema.id, data: getLayout() });
|
||||
}
|
||||
|
||||
function handleCreateSynthesis() {
|
||||
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
showCreateOperation({
|
||||
ossID: schema.id,
|
||||
layout: getLayout(),
|
||||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
initialInputs: selectedItems.filter(item => item?.nodeType === NodeType.OPERATION).map(item => item.id),
|
||||
initialParent: extractBlockParent(selectedItems),
|
||||
onCreate: newID => {
|
||||
resetView();
|
||||
setTimeout(() => setSelected([`o${newID}`]), PARAMETER.minimalTimeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateBlock() {
|
||||
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
const parent = extractBlockParent(selectedItems);
|
||||
const needChildren = parent === null || selectedItems.length !== 1 || parent !== selectedItems[0].id;
|
||||
showCreateBlock({
|
||||
ossID: schema.id,
|
||||
layout: getLayout(),
|
||||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
childrenBlocks: !needChildren
|
||||
? []
|
||||
: selectedItems.filter(item => item.nodeType === NodeType.BLOCK).map(item => item.id),
|
||||
childrenOperations: !needChildren
|
||||
? []
|
||||
: selectedItems.filter(item => item.nodeType === NodeType.OPERATION).map(item => item.id),
|
||||
initialParent: parent,
|
||||
onCreate: newID => {
|
||||
resetView();
|
||||
setTimeout(() => setSelected([`b${newID}`]), PARAMETER.minimalTimeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateSchema() {
|
||||
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
showCreateSchema({
|
||||
ossID: schema.id,
|
||||
layout: getLayout(),
|
||||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
initialParent: extractBlockParent(selectedItems),
|
||||
onCreate: newID => {
|
||||
resetView();
|
||||
setTimeout(() => setSelected([`o${newID}`]), PARAMETER.minimalTimeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleImportSchema() {
|
||||
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
showImportSchema({
|
||||
ossID: schema.id,
|
||||
layout: getLayout(),
|
||||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
initialParent: extractBlockParent(selectedItems),
|
||||
onCreate: newID => {
|
||||
resetView();
|
||||
setTimeout(() => setSelected([`o${newID}`]), PARAMETER.minimalTimeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleDeleteSelected() {
|
||||
if (selected.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const item = schema.itemByNodeID.get(selected[0]);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (item.nodeType === NodeType.OPERATION) {
|
||||
if (!canDeleteOperation(item)) {
|
||||
return;
|
||||
}
|
||||
switch (item.operation_type) {
|
||||
case OperationType.REPLICA:
|
||||
showDeleteReference({
|
||||
ossID: schema.id,
|
||||
targetID: item.id,
|
||||
layout: getLayout(),
|
||||
beforeDelete: deselectAll
|
||||
});
|
||||
break;
|
||||
case OperationType.INPUT:
|
||||
case OperationType.SYNTHESIS:
|
||||
showDeleteOperation({
|
||||
ossID: schema.id,
|
||||
targetID: item.id,
|
||||
layout: getLayout(),
|
||||
beforeDelete: deselectAll
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!window.confirm(promptText.deleteBlock)) {
|
||||
return;
|
||||
}
|
||||
void deleteBlock({
|
||||
itemID: schema.id,
|
||||
data: { target: item.id, layout: getLayout() },
|
||||
beforeUpdate: deselectAll
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectLeft() {
|
||||
const selectedOperation = selectedItems.find(item => item.nodeType === NodeType.OPERATION);
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
const manager = new LayoutManager(schema, getLayout());
|
||||
const newNodeID = manager.selectLeft(selectedOperation.nodeID);
|
||||
if (newNodeID) {
|
||||
setSelected([newNodeID]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectRight() {
|
||||
const selectedOperation = selectedItems.find(item => item.nodeType === NodeType.OPERATION);
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
const manager = new LayoutManager(schema, getLayout());
|
||||
const newNodeID = manager.selectRight(selectedOperation.nodeID);
|
||||
if (newNodeID) {
|
||||
setSelected([newNodeID]);
|
||||
}
|
||||
}
|
||||
function handleSelectUp() {
|
||||
const selectedOperation = selectedItems.find(item => item.nodeType === NodeType.OPERATION);
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
const manager = new LayoutManager(schema, getLayout());
|
||||
const newNodeID = manager.selectUp(selectedOperation.nodeID);
|
||||
if (newNodeID) {
|
||||
setSelected([newNodeID]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectDown() {
|
||||
const selectedOperation = selectedItems.find(item => item.nodeType === NodeType.OPERATION);
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
const manager = new LayoutManager(schema, getLayout());
|
||||
const newNodeID = manager.selectDown(selectedOperation.nodeID);
|
||||
if (newNodeID) {
|
||||
setSelected([newNodeID]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
withPreventDefault(resetSelectedElements)(event);
|
||||
return;
|
||||
}
|
||||
if (!isMutable) {
|
||||
return;
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
||||
withPreventDefault(handleSavePositions)(event);
|
||||
return;
|
||||
}
|
||||
if (event.altKey) {
|
||||
if (event.code === 'Digit1') {
|
||||
withPreventDefault(handleCreateBlock)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Digit2') {
|
||||
withPreventDefault(handleCreateSynthesis)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Digit3') {
|
||||
withPreventDefault(handleImportSchema)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Digit4') {
|
||||
withPreventDefault(handleCreateSynthesis)(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event.code === 'Delete') {
|
||||
withPreventDefault(handleDeleteSelected)(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.code === 'ArrowLeft') {
|
||||
withPreventDefault(handleSelectLeft)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'ArrowRight') {
|
||||
withPreventDefault(handleSelectRight)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'ArrowUp') {
|
||||
withPreventDefault(handleSelectUp)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'ArrowDown') {
|
||||
withPreventDefault(handleSelectDown)(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleKeyDown,
|
||||
|
||||
handleSelectLeft,
|
||||
handleSelectRight,
|
||||
handleSelectUp,
|
||||
handleSelectDown,
|
||||
handleSavePositions,
|
||||
handleCreateSynthesis,
|
||||
handleCreateBlock,
|
||||
handleCreateSchema,
|
||||
handleImportSchema,
|
||||
handleDeleteSelected,
|
||||
handleResetPositions: resetGraph,
|
||||
handleShowOptions
|
||||
};
|
||||
}
|
||||
|
||||
// -------- Internals --------
|
||||
function extractBlockParent(selectedItems: IOssItem[]): number | null {
|
||||
if (selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK) {
|
||||
return selectedItems[0].id;
|
||||
}
|
||||
const parents = selectedItems.map(item => item.parent).filter(id => id !== null);
|
||||
return parents.length === 0 ? null : parents[0];
|
||||
}
|
||||
|
|
@ -114,7 +114,7 @@ export function ToolbarGraphSelection({
|
|||
<div className={cn('cc-icons items-center', className)} {...restProps}>
|
||||
<MiniButton
|
||||
title={!tipHotkeys ? 'Сбросить выделение' : undefined}
|
||||
titleHtml={tipHotkeys ? prepareTooltip('Сбросить выделение', 'Esc') : undefined}
|
||||
titleHtml={tipHotkeys ? prepareTooltip('Сбросить выделение', 'ESC') : undefined}
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectReset}
|
||||
disabled={emptySelection}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function ToolbarRSList({ className }: ToolbarRSListProps) {
|
|||
const { elementRef: menuRef, isOpen: isMenuOpen, toggle: toggleMenu, handleBlur: handleMenuBlur } = useDropdown();
|
||||
const {
|
||||
schema,
|
||||
selectedCst: selected,
|
||||
selectedCst,
|
||||
activeCst,
|
||||
navigateOss,
|
||||
deselectAll,
|
||||
|
|
@ -57,7 +57,7 @@ export function ToolbarRSList({ className }: ToolbarRSListProps) {
|
|||
void updateCrucial({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
target: selected,
|
||||
target: selectedCst,
|
||||
value: !activeCst.crucial
|
||||
}
|
||||
});
|
||||
|
|
@ -76,28 +76,28 @@ export function ToolbarRSList({ className }: ToolbarRSListProps) {
|
|||
aria-label='Сбросить выделение'
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
onClick={deselectAll}
|
||||
disabled={selected.length === 0}
|
||||
disabled={selectedCst.length === 0}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
|
||||
aria-label='Переместить вверх'
|
||||
icon={<IconMoveUp size='1.25rem' className='icon-primary' />}
|
||||
onClick={moveUp}
|
||||
disabled={isProcessing || selected.length === 0 || selected.length === schema.items.length}
|
||||
disabled={isProcessing || selectedCst.length === 0 || selectedCst.length === schema.items.length}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
|
||||
aria-label='Переместить вниз'
|
||||
icon={<IconMoveDown size='1.25rem' className='icon-primary' />}
|
||||
onClick={moveDown}
|
||||
disabled={isProcessing || selected.length === 0 || selected.length === schema.items.length}
|
||||
disabled={isProcessing || selectedCst.length === 0 || selectedCst.length === schema.items.length}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Ключевая конституента'
|
||||
aria-label='Переключатель статуса ключевой конституенты'
|
||||
icon={<IconCrucial size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleToggleCrucial}
|
||||
disabled={isProcessing || selected.length === 0}
|
||||
disabled={isProcessing || selectedCst.length === 0}
|
||||
/>
|
||||
<div ref={menuRef} onBlur={handleMenuBlur} className='relative'>
|
||||
<MiniButton
|
||||
|
|
@ -131,7 +131,7 @@ export function ToolbarRSList({ className }: ToolbarRSListProps) {
|
|||
aria-label='Клонировать конституенту'
|
||||
icon={<IconClone size='1.25rem' className='icon-green' />}
|
||||
onClick={cloneCst}
|
||||
disabled={isProcessing || selected.length !== 1}
|
||||
disabled={isProcessing || selectedCst.length !== 1}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
|
||||
|
|
|
|||
|
|
@ -18,16 +18,13 @@ import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
|
|||
import { useContinuousPan } from '@/components/flow/use-continuous-panning';
|
||||
import { useWindowSize } from '@/hooks/use-window-size';
|
||||
import { useFitHeight, useMainHeight } from '@/stores/app-layout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
import { withPreventDefault } from '@/utils/utils';
|
||||
|
||||
import { CstType, ParsingStatus } from '../../../backend/types';
|
||||
import { ParsingStatus } from '../../../backend/types';
|
||||
import { useCreateAttribution } from '../../../backend/use-create-attribution';
|
||||
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
|
||||
import { useUpdateConstituenta } from '../../../backend/use-update-constituenta';
|
||||
import { useUpdateCrucial } from '../../../backend/use-update-crucial';
|
||||
import { colorGraphEdge } from '../../../colors';
|
||||
import { TGConnectionLine } from '../../../components/term-graph/graph/tg-connection';
|
||||
import { TGEdgeTypes } from '../../../components/term-graph/graph/tg-edge-types';
|
||||
|
|
@ -36,18 +33,17 @@ import { SelectColoring } from '../../../components/term-graph/select-coloring';
|
|||
import { SelectEdgeType } from '../../../components/term-graph/select-edge-type';
|
||||
import { ViewHidden } from '../../../components/term-graph/view-hidden';
|
||||
import { applyLayout, inferEdgeType, type TGNodeData } from '../../../models/graph-api';
|
||||
import { addAliasReference, isBasicConcept } from '../../../models/rsform-api';
|
||||
import { addAliasReference } from '../../../models/rsform-api';
|
||||
import { InteractionMode, TGEdgeType, useTermGraphStore, useTGConnectionStore } from '../../../stores/term-graph';
|
||||
import { useRSEdit } from '../rsedit-context';
|
||||
|
||||
import { ToolbarTermGraph } from './toolbar-term-graph';
|
||||
import { useFilteredGraph } from './use-filtered-graph';
|
||||
|
||||
export const fitViewOptions = { padding: 0.3, duration: PARAMETER.zoomDuration };
|
||||
import { useHandleActions } from './use-handle-actions';
|
||||
|
||||
const flowOptions = {
|
||||
fitView: true,
|
||||
fitViewOptions: fitViewOptions,
|
||||
fitViewOptions: { padding: 0.3, duration: PARAMETER.graphLayoutDuration },
|
||||
edgesFocusable: true,
|
||||
nodesFocusable: false,
|
||||
maxZoom: 3,
|
||||
|
|
@ -64,18 +60,12 @@ export function TGFlow() {
|
|||
useContinuousPan(flowRef);
|
||||
|
||||
const mode = useTermGraphStore(state => state.mode);
|
||||
const toggleMode = useTermGraphStore(state => state.toggleMode);
|
||||
const toggleEdgeType = useTGConnectionStore(state => state.toggleConnectionType);
|
||||
|
||||
const setConnectionStart = useTGConnectionStore(state => state.setStart);
|
||||
const connectionType = useTGConnectionStore(state => state.connectionType);
|
||||
const toggleText = useTermGraphStore(state => state.toggleText);
|
||||
const toggleClustering = useTermGraphStore(state => state.toggleClustering);
|
||||
const toggleHermits = useTermGraphStore(state => state.toggleHermits);
|
||||
|
||||
const showEditCst = useDialogsStore(state => state.showEditCst);
|
||||
const { createAttribution } = useCreateAttribution();
|
||||
const { updateConstituenta } = useUpdateConstituenta();
|
||||
const { updateCrucial } = useUpdateCrucial();
|
||||
|
||||
const isProcessing = useMutatingRSForm();
|
||||
const {
|
||||
|
|
@ -83,15 +73,12 @@ export function TGFlow() {
|
|||
schema,
|
||||
selectedCst,
|
||||
setSelectedCst,
|
||||
promptDeleteSelected,
|
||||
focusCst,
|
||||
setFocus,
|
||||
toggleSelectCst,
|
||||
navigateCst,
|
||||
selectedEdges,
|
||||
setSelectedEdges,
|
||||
deselectAll,
|
||||
createCst
|
||||
setSelectedEdges
|
||||
} = useRSEdit();
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
||||
|
|
@ -100,6 +87,7 @@ export function TGFlow() {
|
|||
const filter = useTermGraphStore(state => state.filter);
|
||||
const { filteredGraph, hidden } = useFilteredGraph();
|
||||
const hiddenHeight = useFitHeight(isSmall ? '15rem + 2px' : '13.5rem + 2px', '4rem');
|
||||
const { handleKeyDown } = useHandleActions(filteredGraph);
|
||||
|
||||
function onSelectionChange({ nodes, edges }: { nodes: Node[]; edges: Edge[] }) {
|
||||
const ids = nodes.map(node => Number(node.id));
|
||||
|
|
@ -224,10 +212,7 @@ export function TGFlow() {
|
|||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(
|
||||
() => fitView({ ...flowOptions.fitViewOptions, duration: PARAMETER.graphLayoutDuration }),
|
||||
PARAMETER.refreshTimeout
|
||||
);
|
||||
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
|
||||
}, [schema.id, filter.noText, filter.graphType, focusCst, fitView]);
|
||||
|
||||
const prevSelectedNodes = useRef<number[]>([]);
|
||||
|
|
@ -269,13 +254,6 @@ export function TGFlow() {
|
|||
);
|
||||
}, [selectedEdges, setEdges]);
|
||||
|
||||
function handleDeleteSelected() {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
promptDeleteSelected();
|
||||
}
|
||||
|
||||
function handleNodeContextMenu(event: React.MouseEvent<Element>, node: TGNodeData) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
|
@ -367,189 +345,6 @@ export function TGFlow() {
|
|||
}
|
||||
}
|
||||
|
||||
function handleToggleMode() {
|
||||
toggleMode();
|
||||
deselectAll();
|
||||
}
|
||||
|
||||
function handleToggleCrucial() {
|
||||
if (selectedCst.length === 0) {
|
||||
return;
|
||||
}
|
||||
const isCrucial = !schema.cstByID.get(selectedCst[0])!.crucial;
|
||||
void updateCrucial({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
target: selectedCst,
|
||||
value: isCrucial
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateCst() {
|
||||
const definition = selectedCst.map(id => schema.cstByID.get(id)!.alias).join(' ');
|
||||
createCst(selectedCst.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
||||
}
|
||||
|
||||
function handelFastEdit() {
|
||||
if (selectedCst.length !== 1) {
|
||||
return;
|
||||
}
|
||||
showEditCst({ schemaID: schema.id, targetID: selectedCst[0] });
|
||||
}
|
||||
|
||||
function handleSelectCore() {
|
||||
const isCore = (cstID: number) => {
|
||||
const cst = schema.cstByID.get(cstID);
|
||||
return !!cst && isBasicConcept(cst.cst_type);
|
||||
};
|
||||
const core = [...filteredGraph.nodes.keys()].filter(isCore);
|
||||
setSelectedCst([...core, ...filteredGraph.expandInputs(core)]);
|
||||
}
|
||||
|
||||
function handleSelectOwned() {
|
||||
setSelectedCst([...filteredGraph.nodes.keys()].filter(cstID => !schema.cstByID.get(cstID)?.is_inherited));
|
||||
}
|
||||
|
||||
function handleSelectInherited() {
|
||||
setSelectedCst([...filteredGraph.nodes.keys()].filter(cstID => schema.cstByID.get(cstID)?.is_inherited ?? false));
|
||||
}
|
||||
|
||||
function handleSelectCrucial() {
|
||||
setSelectedCst([...filteredGraph.nodes.keys()].filter(cstID => schema.cstByID.get(cstID)?.crucial ?? false));
|
||||
}
|
||||
|
||||
function handleExpandOutputs() {
|
||||
setSelectedCst(prev => [...prev, ...filteredGraph.expandOutputs(prev)]);
|
||||
}
|
||||
|
||||
function handleExpandInputs() {
|
||||
setSelectedCst(prev => [...prev, ...filteredGraph.expandInputs(prev)]);
|
||||
}
|
||||
|
||||
function handleSelectMaximize() {
|
||||
setSelectedCst(prev => filteredGraph.maximizePart(prev));
|
||||
}
|
||||
|
||||
function handleSelectInvert() {
|
||||
setSelectedCst(prev => [...filteredGraph.nodes.keys()].filter(item => !prev.includes(item)));
|
||||
}
|
||||
|
||||
function handleSelectAllInputs() {
|
||||
setSelectedCst(prev => [...prev, ...filteredGraph.expandAllInputs(prev)]);
|
||||
}
|
||||
|
||||
function handleSelectAllOutputs() {
|
||||
setSelectedCst(prev => [...prev, ...filteredGraph.expandAllOutputs(prev)]);
|
||||
}
|
||||
|
||||
function handleSelectionHotkey(eventCode: string): boolean {
|
||||
if (eventCode === 'Escape') {
|
||||
setFocus(null);
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit1') {
|
||||
handleExpandInputs();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit2') {
|
||||
handleExpandOutputs();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit3') {
|
||||
handleSelectAllInputs();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit4') {
|
||||
handleSelectAllOutputs();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit5') {
|
||||
handleSelectMaximize();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit6') {
|
||||
handleSelectInvert();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'KeyZ') {
|
||||
handleSelectCore();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'KeyX') {
|
||||
handleSelectCrucial();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'KeyC') {
|
||||
handleSelectOwned();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'KeyY') {
|
||||
handleSelectInherited();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) {
|
||||
return;
|
||||
}
|
||||
if (handleSelectionHotkey(event.code)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.code === 'KeyG') {
|
||||
withPreventDefault(() => fitView(flowOptions.fitViewOptions))(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyT') {
|
||||
withPreventDefault(toggleText)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyB') {
|
||||
withPreventDefault(toggleClustering)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyH') {
|
||||
withPreventDefault(toggleHermits)(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isContentEditable) {
|
||||
if (event.code === 'KeyF') {
|
||||
withPreventDefault(handleToggleCrucial)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyQ') {
|
||||
withPreventDefault(handleToggleMode)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyE' && mode === InteractionMode.edit) {
|
||||
withPreventDefault(toggleEdgeType)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyR') {
|
||||
withPreventDefault(handleCreateCst)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyV') {
|
||||
withPreventDefault(handelFastEdit)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Delete' || event.code === 'Backquote') {
|
||||
withPreventDefault(handleDeleteSelected)(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={flowRef}
|
||||
|
|
@ -561,11 +356,7 @@ export function TGFlow() {
|
|||
tabIndex={-1}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<ToolbarTermGraph
|
||||
className='cc-tab-tools'
|
||||
onDeleteSelected={handleDeleteSelected}
|
||||
onToggleCrucial={handleToggleCrucial}
|
||||
/>
|
||||
<ToolbarTermGraph className='cc-tab-tools' graph={filteredGraph} />
|
||||
|
||||
<div className='absolute z-pop top-24 sm:top-16 left-2 sm:left-3 w-54 flex flex-col pointer-events-none'>
|
||||
<span className='px-2 pb-1 select-none whitespace-nowrap backdrop-blur-xs rounded-xl w-fit'>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useReactFlow, useStoreApi } from 'reactflow';
|
||||
import { useStoreApi } from 'reactflow';
|
||||
|
||||
import { HelpTopic } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
||||
|
|
@ -22,11 +22,10 @@ import {
|
|||
IconTypeGraph
|
||||
} from '@/components/icons';
|
||||
import { cn } from '@/components/utils';
|
||||
import { type Graph } from '@/models/graph';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { prepareTooltip } from '@/utils/utils';
|
||||
|
||||
import { CstType } from '../../../backend/types';
|
||||
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
|
||||
import { IconEdgeType } from '../../../components/icon-edge-type';
|
||||
import { IconGraphMode } from '../../../components/icon-graph-mode';
|
||||
|
|
@ -38,75 +37,39 @@ import { isBasicConcept } from '../../../models/rsform-api';
|
|||
import { InteractionMode, useTermGraphStore, useTGConnectionStore } from '../../../stores/term-graph';
|
||||
import { useRSEdit } from '../rsedit-context';
|
||||
|
||||
import { fitViewOptions } from './tg-flow';
|
||||
import { useFilteredGraph } from './use-filtered-graph';
|
||||
import { useHandleActions } from './use-handle-actions';
|
||||
|
||||
interface ToolbarTermGraphProps {
|
||||
className?: string;
|
||||
|
||||
onDeleteSelected: () => void;
|
||||
onToggleCrucial: () => void;
|
||||
graph: Graph<number>;
|
||||
}
|
||||
|
||||
export function ToolbarTermGraph({ className, onDeleteSelected, onToggleCrucial }: ToolbarTermGraphProps) {
|
||||
export function ToolbarTermGraph({ className, graph }: ToolbarTermGraphProps) {
|
||||
const isProcessing = useMutatingRSForm();
|
||||
const {
|
||||
schema,
|
||||
selectedCst,
|
||||
setSelectedCst,
|
||||
setFocus,
|
||||
navigateOss,
|
||||
isContentEditable,
|
||||
canDeleteSelected,
|
||||
createCst,
|
||||
focusCst,
|
||||
deselectAll
|
||||
} = useRSEdit();
|
||||
const { schema, selectedCst, setSelectedCst, setFocus, navigateOss, isContentEditable, canDeleteSelected, focusCst } =
|
||||
useRSEdit();
|
||||
|
||||
const {
|
||||
handleShowTypeGraph,
|
||||
handleSetFocus,
|
||||
handleFitView,
|
||||
handleToggleMode,
|
||||
handleToggleCrucial,
|
||||
handleCreateCst,
|
||||
handleDeleteSelected,
|
||||
handleToggleEdgeType,
|
||||
handleToggleText,
|
||||
handleToggleClustering
|
||||
} = useHandleActions(graph);
|
||||
|
||||
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
|
||||
const showParams = useDialogsStore(state => state.showGraphParams);
|
||||
const mode = useTermGraphStore(state => state.mode);
|
||||
const toggleMode = useTermGraphStore(state => state.toggleMode);
|
||||
const edgeType = useTGConnectionStore(state => state.connectionType);
|
||||
const toggleEdgeType = useTGConnectionStore(state => state.toggleConnectionType);
|
||||
const filter = useTermGraphStore(state => state.filter);
|
||||
const toggleText = useTermGraphStore(state => state.toggleText);
|
||||
const toggleClustering = useTermGraphStore(state => state.toggleClustering);
|
||||
const { filteredGraph } = useFilteredGraph();
|
||||
|
||||
const { fitView } = useReactFlow();
|
||||
const store = useStoreApi();
|
||||
const { addSelectedNodes } = store.getState();
|
||||
|
||||
function handleShowTypeGraph() {
|
||||
const typeInfo = schema.items
|
||||
.filter(item => !!item.parse)
|
||||
.map(item => ({
|
||||
alias: item.alias,
|
||||
result: item.parse!.typification,
|
||||
args: item.parse!.args
|
||||
}));
|
||||
showTypeGraph({ items: typeInfo });
|
||||
}
|
||||
|
||||
function handleCreateCst() {
|
||||
const definition = selectedCst.map(id => schema.cstByID.get(id)!.alias).join(' ');
|
||||
createCst(selectedCst.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
||||
}
|
||||
|
||||
function handleFitView() {
|
||||
setTimeout(() => {
|
||||
fitView(fitViewOptions);
|
||||
}, PARAMETER.minimalTimeout);
|
||||
}
|
||||
|
||||
function handleSetFocus() {
|
||||
const target = schema.cstByID.get(selectedCst[0]);
|
||||
if (target) {
|
||||
setFocus(target);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectOss(event: React.MouseEvent<HTMLElement>, newValue: ILibraryItemReference) {
|
||||
navigateOss(newValue.id, event.ctrlKey || event.metaKey);
|
||||
}
|
||||
|
|
@ -116,11 +79,6 @@ export function ToolbarTermGraph({ className, onDeleteSelected, onToggleCrucial
|
|||
addSelectedNodes(newSelection.map(id => String(id)));
|
||||
}
|
||||
|
||||
function handleToggleMode() {
|
||||
toggleMode();
|
||||
deselectAll();
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -156,7 +114,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected, onToggleCrucial
|
|||
<IconTextOff size='1.25rem' className='icon-primary' />
|
||||
)
|
||||
}
|
||||
onClick={toggleText}
|
||||
onClick={handleToggleText}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip(!filter.foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные', 'V')}
|
||||
|
|
@ -167,7 +125,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected, onToggleCrucial
|
|||
<IconClusteringOff size='1.25rem' className='icon-primary' />
|
||||
)
|
||||
}
|
||||
onClick={toggleClustering}
|
||||
onClick={handleToggleClustering}
|
||||
/>
|
||||
|
||||
<BadgeHelp topic={HelpTopic.UI_GRAPH_TERM} contentClass='sm:max-w-160' offset={4} />
|
||||
|
|
@ -177,7 +135,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected, onToggleCrucial
|
|||
{!focusCst && mode === InteractionMode.explore ? (
|
||||
<ToolbarGraphSelection
|
||||
tipHotkeys
|
||||
graph={filteredGraph}
|
||||
graph={graph}
|
||||
isCore={cstID => {
|
||||
const cst = schema.cstByID.get(cstID);
|
||||
return !!cst && isBasicConcept(cst.cst_type);
|
||||
|
|
@ -193,7 +151,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected, onToggleCrucial
|
|||
titleHtml={prepareTooltip('Ключевая конституента', 'F')}
|
||||
aria-label='Переключатель статуса ключевой конституенты'
|
||||
icon={<IconCrucial size='1.25rem' className='icon-primary' />}
|
||||
onClick={onToggleCrucial}
|
||||
onClick={handleToggleCrucial}
|
||||
disabled={isProcessing || selectedCst.length === 0}
|
||||
/>
|
||||
) : null}
|
||||
|
|
@ -209,7 +167,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected, onToggleCrucial
|
|||
{isContentEditable && mode === InteractionMode.edit ? (
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip(labelEdgeType(edgeType), 'E')}
|
||||
onClick={toggleEdgeType}
|
||||
onClick={handleToggleEdgeType}
|
||||
icon={<IconEdgeType value={edgeType} size='1.25rem' className='icon-primary' />}
|
||||
/>
|
||||
) : null}
|
||||
|
|
@ -225,7 +183,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected, onToggleCrucial
|
|||
<MiniButton
|
||||
titleHtml={prepareTooltip('Удалить выбранные', 'Delete, `')}
|
||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||
onClick={onDeleteSelected}
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={!canDeleteSelected || isProcessing}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,281 @@
|
|||
import { useReactFlow } from 'reactflow';
|
||||
|
||||
import { type Graph } from '@/models/graph';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { withPreventDefault } from '@/utils/utils';
|
||||
|
||||
import { CstType } from '../../../backend/types';
|
||||
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
|
||||
import { useUpdateCrucial } from '../../../backend/use-update-crucial';
|
||||
import { isBasicConcept } from '../../../models/rsform-api';
|
||||
import { InteractionMode, useTermGraphStore, useTGConnectionStore } from '../../../stores/term-graph';
|
||||
import { useRSEdit } from '../rsedit-context';
|
||||
|
||||
/** Options for graph fit view. */
|
||||
export const fitViewOptions = { padding: 0.3, duration: PARAMETER.zoomDuration };
|
||||
|
||||
export function useHandleActions(graph: Graph<number>) {
|
||||
const isProcessing = useMutatingRSForm();
|
||||
const { fitView } = useReactFlow();
|
||||
const {
|
||||
schema,
|
||||
selectedCst,
|
||||
isContentEditable,
|
||||
setSelectedCst,
|
||||
deselectAll,
|
||||
createCst,
|
||||
setFocus,
|
||||
promptDeleteSelected
|
||||
} = useRSEdit();
|
||||
|
||||
const mode = useTermGraphStore(state => state.mode);
|
||||
|
||||
const toggleText = useTermGraphStore(state => state.toggleText);
|
||||
const toggleClustering = useTermGraphStore(state => state.toggleClustering);
|
||||
const toggleHermits = useTermGraphStore(state => state.toggleHermits);
|
||||
const toggleMode = useTermGraphStore(state => state.toggleMode);
|
||||
const toggleEdgeType = useTGConnectionStore(state => state.toggleConnectionType);
|
||||
|
||||
const { updateCrucial } = useUpdateCrucial();
|
||||
const showEditCst = useDialogsStore(state => state.showEditCst);
|
||||
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
|
||||
|
||||
function handleShowTypeGraph() {
|
||||
const typeInfo = schema.items
|
||||
.filter(item => !!item.parse)
|
||||
.map(item => ({
|
||||
alias: item.alias,
|
||||
result: item.parse!.typification,
|
||||
args: item.parse!.args
|
||||
}));
|
||||
showTypeGraph({ items: typeInfo });
|
||||
}
|
||||
|
||||
function handleSetFocus() {
|
||||
const target = schema.cstByID.get(selectedCst[0]);
|
||||
if (target) {
|
||||
setFocus(target);
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleMode() {
|
||||
toggleMode();
|
||||
deselectAll();
|
||||
}
|
||||
|
||||
function handleToggleCrucial() {
|
||||
if (selectedCst.length === 0) {
|
||||
return;
|
||||
}
|
||||
const isCrucial = !schema.cstByID.get(selectedCst[0])!.crucial;
|
||||
void updateCrucial({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
target: selectedCst,
|
||||
value: isCrucial
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateCst() {
|
||||
const definition = selectedCst.map(id => schema.cstByID.get(id)!.alias).join(' ');
|
||||
createCst(selectedCst.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
||||
}
|
||||
|
||||
function handleDeleteSelected() {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
promptDeleteSelected();
|
||||
}
|
||||
|
||||
function handelFastEdit() {
|
||||
if (selectedCst.length !== 1) {
|
||||
return;
|
||||
}
|
||||
showEditCst({ schemaID: schema.id, targetID: selectedCst[0] });
|
||||
}
|
||||
|
||||
function handleSelectCore() {
|
||||
const isCore = (cstID: number) => {
|
||||
const cst = schema.cstByID.get(cstID);
|
||||
return !!cst && isBasicConcept(cst.cst_type);
|
||||
};
|
||||
const core = [...graph.nodes.keys()].filter(isCore);
|
||||
setSelectedCst([...core, ...graph.expandInputs(core)]);
|
||||
}
|
||||
|
||||
function handleSelectOwned() {
|
||||
setSelectedCst([...graph.nodes.keys()].filter(cstID => !schema.cstByID.get(cstID)?.is_inherited));
|
||||
}
|
||||
|
||||
function handleSelectInherited() {
|
||||
setSelectedCst([...graph.nodes.keys()].filter(cstID => schema.cstByID.get(cstID)?.is_inherited ?? false));
|
||||
}
|
||||
|
||||
function handleSelectCrucial() {
|
||||
setSelectedCst([...graph.nodes.keys()].filter(cstID => schema.cstByID.get(cstID)?.crucial ?? false));
|
||||
}
|
||||
|
||||
function handleExpandOutputs() {
|
||||
setSelectedCst(prev => [...prev, ...graph.expandOutputs(prev)]);
|
||||
}
|
||||
|
||||
function handleExpandInputs() {
|
||||
setSelectedCst(prev => [...prev, ...graph.expandInputs(prev)]);
|
||||
}
|
||||
|
||||
function handleSelectMaximize() {
|
||||
setSelectedCst(prev => graph.maximizePart(prev));
|
||||
}
|
||||
|
||||
function handleSelectInvert() {
|
||||
setSelectedCst(prev => [...graph.nodes.keys()].filter(item => !prev.includes(item)));
|
||||
}
|
||||
|
||||
function handleSelectAllInputs() {
|
||||
setSelectedCst(prev => [...prev, ...graph.expandAllInputs(prev)]);
|
||||
}
|
||||
|
||||
function handleSelectAllOutputs() {
|
||||
setSelectedCst(prev => [...prev, ...graph.expandAllOutputs(prev)]);
|
||||
}
|
||||
|
||||
function handleFitView() {
|
||||
fitView(fitViewOptions);
|
||||
}
|
||||
|
||||
function handleSelectionHotkey(eventCode: string): boolean {
|
||||
if (eventCode === 'Escape') {
|
||||
setFocus(null);
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit1') {
|
||||
handleExpandInputs();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit2') {
|
||||
handleExpandOutputs();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit3') {
|
||||
handleSelectAllInputs();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit4') {
|
||||
handleSelectAllOutputs();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit5') {
|
||||
handleSelectMaximize();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit6') {
|
||||
handleSelectInvert();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'KeyZ') {
|
||||
handleSelectCore();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'KeyX') {
|
||||
handleSelectCrucial();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'KeyC') {
|
||||
handleSelectOwned();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'KeyY') {
|
||||
handleSelectInherited();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) {
|
||||
return;
|
||||
}
|
||||
if (handleSelectionHotkey(event.code)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.code === 'KeyG') {
|
||||
withPreventDefault(handleFitView)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyT') {
|
||||
withPreventDefault(toggleText)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyB') {
|
||||
withPreventDefault(toggleClustering)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyH') {
|
||||
withPreventDefault(toggleHermits)(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isContentEditable) {
|
||||
if (event.code === 'KeyF') {
|
||||
withPreventDefault(handleToggleCrucial)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyQ') {
|
||||
withPreventDefault(handleToggleMode)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyE' && mode === InteractionMode.edit) {
|
||||
withPreventDefault(toggleEdgeType)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyR') {
|
||||
withPreventDefault(handleCreateCst)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyV') {
|
||||
withPreventDefault(handelFastEdit)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Delete' || event.code === 'Backquote') {
|
||||
withPreventDefault(handleDeleteSelected)(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleKeyDown,
|
||||
|
||||
handleShowTypeGraph,
|
||||
handleSetFocus,
|
||||
handleFitView,
|
||||
handleExpandInputs,
|
||||
handleExpandOutputs,
|
||||
handleSelectAllInputs,
|
||||
handleSelectAllOutputs,
|
||||
handleSelectMaximize,
|
||||
handleSelectInvert,
|
||||
handleSelectCore,
|
||||
handleSelectOwned,
|
||||
handleSelectInherited,
|
||||
handleSelectCrucial,
|
||||
handleToggleMode,
|
||||
handleToggleCrucial,
|
||||
handleCreateCst,
|
||||
handleDeleteSelected,
|
||||
handleToggleEdgeType: toggleEdgeType,
|
||||
handleToggleText: toggleText,
|
||||
handleToggleClustering: toggleClustering,
|
||||
handleToggleHermits: toggleHermits
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user