R: Extract action handlers

This commit is contained in:
Ivan 2025-11-20 13:15:00 +03:00
parent 86ac5e5508
commit c9bc3401b0
9 changed files with 636 additions and 596 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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