F: Improve termgraph UI accelerators

This commit is contained in:
Ivan 2025-11-17 18:44:46 +03:00
parent f63d9cf93e
commit 584f62579d
14 changed files with 311 additions and 51 deletions

View File

@ -0,0 +1,79 @@
import { useCallback, useEffect, useRef } from 'react';
import { useReactFlow } from 'reactflow';
interface PanOptions {
panSpeed: number;
}
export function useContinuousPan(
ref: React.RefObject<HTMLDivElement | null>,
options: PanOptions = {
panSpeed: 15
}
) {
const { getViewport, setViewport } = useReactFlow();
const keysPressed = useRef<Set<string>>(new Set());
const rafRef = useRef<number | null>(null);
const panLoop = useCallback(() => {
const viewport = getViewport();
let { x, y } = viewport;
if (keysPressed.current.has('KeyW')) y += options.panSpeed;
if (keysPressed.current.has('KeyS')) y -= options.panSpeed;
if (keysPressed.current.has('KeyA')) x += options.panSpeed;
if (keysPressed.current.has('KeyD')) x -= options.panSpeed;
setViewport({ x, y, zoom: viewport.zoom }, { duration: 0 });
// eslint-disable-next-line react-hooks/immutability
rafRef.current = requestAnimationFrame(() => panLoop());
}, [options.panSpeed, getViewport, setViewport]);
useEffect(() => {
const element = ref.current;
if (!element) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
if (!['KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(event.code)) return;
event.preventDefault();
event.stopPropagation();
keysPressed.current.add(event.code);
if (rafRef.current === null) {
rafRef.current = requestAnimationFrame(panLoop);
}
};
const handleKeyUp = (event: KeyboardEvent) => {
keysPressed.current.delete(event.code);
if (keysPressed.current.size === 0 && rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
};
const handleBlur = () => {
keysPressed.current.clear();
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
};
element.addEventListener('keydown', handleKeyDown, { passive: false });
element.addEventListener('keyup', handleKeyUp);
element.addEventListener('blur', handleBlur);
return () => {
element.removeEventListener('keydown', handleKeyDown);
element.removeEventListener('keyup', handleKeyUp);
element.removeEventListener('blur', handleBlur);
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, [ref, panLoop]);
}

View File

@ -1,3 +1,7 @@
import { IconEdgeType } from '@/features/rsform/components/icon-edge-type';
import { IconGraphMode } from '@/features/rsform/components/icon-graph-mode';
import { InteractionMode, TGEdgeType } from '@/features/rsform/stores/term-graph';
import { Divider } from '@/components/container';
import {
IconChild,
@ -80,6 +84,9 @@ export function HelpRSGraphTerm() {
<li>
<kbd>Space</kbd> перемещение экрана
</li>
<li>
<kbd>WASD</kbd> - направленное перемещение
</li>
<li>
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
</li>
@ -96,6 +103,18 @@ export function HelpRSGraphTerm() {
<IconTypeGraph className='inline-icon' /> Открыть{' '}
<LinkTopic text='граф ступеней' topic={HelpTopic.UI_TYPE_GRAPH} />
</li>
<li>
<IconGraphMode value={InteractionMode.explore} className='inline-icon' /> Просмотр графа
</li>
<li>
<IconGraphMode value={InteractionMode.edit} className='inline-icon icon-green' /> Редактирование связей
</li>
<li>
<IconEdgeType value={TGEdgeType.attribution} className='inline-icon' /> Атрибутирование
</li>
<li>
<IconEdgeType value={TGEdgeType.definition} className='inline-icon' /> Определение
</li>
</ul>
</div>

View File

@ -22,7 +22,6 @@ export class RSFormLoader {
private schema: IRSForm;
private graph: Graph = new Graph();
private association_graph: Graph = new Graph();
private full_graph: Graph = new Graph();
private cstByAlias = new Map<string, IConstituenta>();
private cstByID = new Map<number, IConstituenta>();
@ -42,7 +41,6 @@ export class RSFormLoader {
result.cstByAlias = this.cstByAlias;
result.cstByID = this.cstByID;
result.attribution_graph = this.association_graph;
result.full_graph = this.full_graph;
return result;
}
@ -52,7 +50,6 @@ export class RSFormLoader {
this.cstByID.set(cst.id, cst);
this.graph.addNode(cst.id);
this.association_graph.addNode(cst.id);
this.full_graph.addNode(cst.id);
});
}
@ -63,7 +60,6 @@ export class RSFormLoader {
const source = this.cstByAlias.get(alias);
if (source) {
this.graph.addEdge(source.id, cst.id);
this.full_graph.addEdge(source.id, cst.id);
}
});
});
@ -113,9 +109,6 @@ export class RSFormLoader {
this.schema.attribution.forEach(attrib => {
const container = this.cstByID.get(attrib.container)!;
container.attributes.push(attrib.attribute);
if (!this.full_graph.hasEdge(attrib.attribute, attrib.container)) {
this.full_graph.addEdge(attrib.container, attrib.attribute);
}
this.association_graph.addEdge(attrib.container, attrib.attribute);
});
}

View File

@ -200,7 +200,7 @@ export function colorBgCstClass(cstClass: CstClass): string {
case CstClass.BASIC: return APP_COLORS.bgGreen;
case CstClass.DERIVED: return APP_COLORS.bgBlue;
case CstClass.STATEMENT: return APP_COLORS.bgRed;
case CstClass.TEMPLATE: return APP_COLORS.bgTeal;
case CstClass.TEMPLATE: return APP_COLORS.bgPurple;
}
}

View File

@ -29,6 +29,7 @@ export function TermGraphEdge({ id, markerEnd, style, ...props }: EdgeProps) {
return (
<>
<path id={id} className='react-flow__edge-path' d={path} markerEnd={markerEnd} style={style} />
<path d={path} className='rf-edge-events cursor-pointer!' />
</>
);
}

View File

@ -18,6 +18,7 @@ import {
import { type Styling } from '@/components/props';
import { cn } from '@/components/utils';
import { type Graph } from '@/models/graph';
import { prepareTooltip } from '@/utils/utils';
interface ToolbarGraphSelectionProps extends Styling {
value: number[];
@ -26,12 +27,14 @@ interface ToolbarGraphSelectionProps extends Styling {
isCore: (item: number) => boolean;
isCrucial: (item: number) => boolean;
isInherited: (item: number) => boolean;
tipHotkeys?: boolean;
}
export function ToolbarGraphSelection({
className,
graph,
value,
tipHotkeys,
isCore,
isInherited,
isCrucial,
@ -110,7 +113,8 @@ export function ToolbarGraphSelection({
return (
<div className={cn('cc-icons items-center', className)} {...restProps}>
<MiniButton
title='Сбросить выделение'
title={!tipHotkeys ? 'Сбросить выделение' : undefined}
titleHtml={tipHotkeys ? prepareTooltip('Сбросить выделение', 'Esc') : undefined}
icon={<IconReset size='1.25rem' className='icon-primary' />}
onClick={handleSelectReset}
disabled={emptySelection}
@ -127,14 +131,16 @@ export function ToolbarGraphSelection({
<Dropdown isOpen={isSelectedOpen} className='-translate-x-1/2'>
<DropdownButton
text='Поставщики'
title='Выделить поставщиков'
title={!tipHotkeys ? 'Выделить поставщиков' : undefined}
titleHtml={tipHotkeys ? prepareTooltip('Выделить поставщиков', '1') : undefined}
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
onClick={handleExpandInputs}
disabled={emptySelection}
/>
<DropdownButton
text='Потребители'
title='Выделить потребителей'
title={!tipHotkeys ? 'Выделить потребителей' : undefined}
titleHtml={tipHotkeys ? prepareTooltip('Выделить потребителей', '2') : undefined}
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
onClick={handleExpandOutputs}
disabled={emptySelection}
@ -142,14 +148,16 @@ export function ToolbarGraphSelection({
<DropdownButton
text='Влияющие'
title='Выделить все влияющие'
title={!tipHotkeys ? 'Выделить все влияющие' : undefined}
titleHtml={tipHotkeys ? prepareTooltip('Выделить все влияющие', '3') : undefined}
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
onClick={handleSelectAllInputs}
disabled={emptySelection}
/>
<DropdownButton
text='Зависимые'
title='Выделить все зависимые'
title={!tipHotkeys ? 'Выделить все зависимые' : undefined}
titleHtml={tipHotkeys ? prepareTooltip('Выделить все зависимые', '4') : undefined}
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
onClick={handleSelectAllOutputs}
disabled={emptySelection}
@ -157,7 +165,14 @@ export function ToolbarGraphSelection({
<DropdownButton
text='Максимизация'
titleHtml='<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
titleHtml={
!tipHotkeys
? '<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
: prepareTooltip(
'Максимизация - дополнение выделения конституентами, зависимыми только от выделенных',
'5'
)
}
aria-label='Максимизация - дополнение выделения конституентами, зависимыми только от выделенных'
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
onClick={handleSelectMaximize}
@ -165,6 +180,7 @@ export function ToolbarGraphSelection({
/>
<DropdownButton
text='Инвертировать'
titleHtml={tipHotkeys ? prepareTooltip('Инвертировать', '6') : undefined}
icon={<IconGraphInverse size='1.25rem' className='icon-primary' />}
onClick={handleSelectInvert}
/>
@ -181,25 +197,29 @@ export function ToolbarGraphSelection({
<Dropdown isOpen={isGroupOpen} stretchLeft>
<DropdownButton
text='ядро'
title='Выделить ядро'
title={!tipHotkeys ? 'Выделить ядро' : undefined}
titleHtml={tipHotkeys ? prepareTooltip('Выделить ядро', 'Z') : undefined}
icon={<IconGraphCore size='1.25rem' className='icon-primary' />}
onClick={handleSelectCore}
/>
<DropdownButton
text='ключевые'
title='Выделить ключевые'
title={!tipHotkeys ? 'Выделить ключевые' : undefined}
titleHtml={tipHotkeys ? prepareTooltip('Выделить ключевые', 'X') : undefined}
icon={<IconCrucial size='1.25rem' className='icon-primary' />}
onClick={handleSelectCrucial}
/>
<DropdownButton
text='собственные'
title='Выделить собственные'
title={!tipHotkeys ? 'Выделить собственные' : undefined}
titleHtml={tipHotkeys ? prepareTooltip('Выделить собственные', 'C') : undefined}
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
onClick={handleSelectOwned}
/>
<DropdownButton
text='наследники'
title='Выделить наследников'
title={!tipHotkeys ? 'Выделить наследников' : undefined}
titleHtml={tipHotkeys ? prepareTooltip('Выделить наследников', 'Y') : undefined}
icon={<IconChild size='1.25rem' className='icon-primary' />}
onClick={handleSelectInherited}
/>

View File

@ -5,6 +5,7 @@ import { Controller, useForm } from 'react-hook-form';
import { MiniButton } from '@/components/control';
import { Checkbox } from '@/components/input';
import { ModalForm } from '@/components/modal';
import { prepareTooltip } from '@/utils/utils';
import { CstType } from '../backend/types';
import { IconCstType } from '../components/icon-cst-type';
@ -35,19 +36,31 @@ export function DlgGraphParams() {
<Controller
control={control}
name='noText'
render={({ field }) => <Checkbox {...field} label='Скрыть текст' title='Не отображать термины' />}
render={({ field }) => (
<Checkbox {...field} label='Скрыть текст' titleHtml={prepareTooltip('Не отображать термины', 'T')} />
)}
/>
<Controller
control={control}
name='foldDerived'
render={({ field }) => (
<Checkbox {...field} label='Скрыть порожденные' title='Не отображать порожденные понятия' />
<Checkbox
{...field}
label='Скрыть порожденные'
titleHtml={prepareTooltip('Не отображать порожденные понятия', 'V')}
/>
)}
/>
<Controller
control={control}
name='noHermits'
render={({ field }) => <Checkbox {...field} label='Скрыть несвязанные' title='Неиспользуемые конституенты' />}
render={({ field }) => (
<Checkbox
{...field}
label='Скрыть несвязанные'
titleHtml={prepareTooltip('Неиспользуемые конституенты', 'B')}
/>
)}
/>
<Controller
control={control}
@ -72,9 +85,9 @@ export function DlgGraphParams() {
)}
/>
</div>
<div className='flex flex-col gap-1'>
<div className='flex flex-col items-center gap-1'>
<h1 className='mb-1'>Типы конституент</h1>
<div>
<div className='grid grid-cols-3'>
{Object.values(CstType).map(cstType => {
const fieldName = cstTypeToFilterKey[cstType];
return (

View File

@ -328,7 +328,8 @@ export function sortItemsForInlineSynthesis(receiver: IRSForm, items: readonly I
/** Remove alias from expression. */
export function removeAliasReference(expression: string, alias: string): string {
return expression.replaceAll(new RegExp(`\\b${alias}\\b`, 'g'), 'DEL');
const result = expression.replaceAll(new RegExp(`\\b${alias}\\b`, 'g'), 'DEL');
return result === 'DEL' ? '' : result;
}
/** Add alias to expression. */

View File

@ -148,7 +148,6 @@ export interface IRSForm extends ILibraryItemData {
stats: IRSFormStats;
graph: Graph;
attribution_graph: Graph;
full_graph: Graph;
cstByAlias: Map<string, IConstituenta>;
cstByID: Map<number, IConstituenta>;
}

View File

@ -15,13 +15,14 @@ import {
import clsx from 'clsx';
import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
import { useContinuousPan } from '@/components/flow/use-continous-panning';
import { useWindowSize } from '@/hooks/use-window-size';
import { useFitHeight, useMainHeight } from '@/stores/app-layout';
import { PARAMETER } from '@/utils/constants';
import { errorMsg } from '@/utils/labels';
import { withPreventDefault } from '@/utils/utils';
import { ParsingStatus } from '../../../backend/types';
import { CstType, 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';
@ -33,7 +34,7 @@ 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 } from '../../../models/rsform-api';
import { addAliasReference, isBasicConcept } from '../../../models/rsform-api';
import { InteractionMode, TGEdgeType, useTermGraphStore, useTGConnectionStore } from '../../../stores/term-graph';
import { useRSEdit } from '../rsedit-context';
@ -55,6 +56,9 @@ export function TGFlow() {
const { isSmall } = useWindowSize();
const mainHeight = useMainHeight();
const { fitView, viewportInitialized } = useReactFlow();
const flowRef = useRef<HTMLDivElement>(null);
useContinuousPan(flowRef);
const mode = useTermGraphStore(state => state.mode);
const toggleMode = useTermGraphStore(state => state.toggleMode);
@ -62,6 +66,8 @@ export function TGFlow() {
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 { createAttribution } = useCreateAttribution();
const { updateConstituenta } = useUpdateConstituenta();
@ -79,7 +85,8 @@ export function TGFlow() {
navigateCst,
selectedEdges,
setSelectedEdges,
deselectAll
deselectAll,
createCst
} = useRSEdit();
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
@ -90,14 +97,9 @@ export function TGFlow() {
const hiddenHeight = useFitHeight(isSmall ? '15rem + 2px' : '13.5rem + 2px', '4rem');
function onSelectionChange({ nodes, edges }: { nodes: Node[]; edges: Edge[] }) {
if (mode === InteractionMode.explore) {
const ids = nodes.map(node => Number(node.id));
setSelectedCst(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]);
setSelectedEdges([]);
} else {
setSelectedCst([]);
setSelectedEdges(edges.map(edge => edge.id));
}
const ids = nodes.map(node => Number(node.id));
setSelectedCst(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]);
setSelectedEdges(edges.map(edge => edge.id));
}
useOnSelectionChange({
onChange: onSelectionChange
@ -294,14 +296,118 @@ export function TGFlow() {
deselectAll();
}
function handleCreateCst() {
const definition = selectedCst.map(id => schema.cstByID.get(id)!.alias).join(' ');
createCst(selectedCst.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
}
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.code === 'Escape') {
withPreventDefault(() => setFocus(null))(event);
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;
@ -310,8 +416,16 @@ export function TGFlow() {
withPreventDefault(toggleText)(event);
return;
}
if (event.code === 'KeyV') {
withPreventDefault(toggleClustering)(event);
return;
}
if (event.code === 'KeyB') {
withPreventDefault(toggleHermits)(event);
return;
}
if (isContentEditable && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
if (isContentEditable) {
if (event.code === 'KeyQ') {
withPreventDefault(handleToggleMode)(event);
return;
@ -320,7 +434,11 @@ export function TGFlow() {
withPreventDefault(toggleEdgeType)(event);
return;
}
if (event.code === 'Delete') {
if (event.code === 'KeyR') {
withPreventDefault(handleCreateCst)(event);
return;
}
if (event.code === 'Delete' || event.code === 'Backquote') {
withPreventDefault(handleDeleteSelected)(event);
return;
}
@ -329,6 +447,7 @@ export function TGFlow() {
return (
<div
ref={flowRef}
className={clsx('relative', mode === InteractionMode.explore ? 'mode-explore' : 'mode-edit')}
tabIndex={-1}
onKeyDown={handleKeyDown}

View File

@ -157,7 +157,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected }: ToolbarTermGra
onClick={toggleText}
/>
<MiniButton
title={!filter.foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные'}
titleHtml={prepareTooltip(!filter.foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные', 'V')}
icon={
!filter.foldDerived ? (
<IconClustering size='1.25rem' className='icon-green' />
@ -174,6 +174,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected }: ToolbarTermGra
{focusCst ? <ToolbarFocusedCst resetFocus={() => setFocus(null)} /> : null}
{!focusCst && mode === InteractionMode.explore ? (
<ToolbarGraphSelection
tipHotkeys
graph={filteredGraph}
isCore={cstID => {
const cst = schema.cstByID.get(cstID);
@ -203,7 +204,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected }: ToolbarTermGra
) : null}
{isContentEditable ? (
<MiniButton
title='Новая конституента'
titleHtml={prepareTooltip('Новая конституента', 'R')}
icon={<IconNewItem size='1.25rem' className='icon-green' />}
onClick={handleCreateCst}
disabled={isProcessing}
@ -211,7 +212,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected }: ToolbarTermGra
) : null}
{isContentEditable ? (
<MiniButton
title='Удалить выбранные'
titleHtml={prepareTooltip('Удалить выбранные', 'Delete, `')}
icon={<IconDestroy size='1.25rem' className='icon-red' />}
onClick={onDeleteSelected}
disabled={!canDeleteSelected || isProcessing}

View File

@ -26,7 +26,6 @@ import { useRSFormSuspense } from '../../backend/use-rsform';
import { useUpdateConstituenta } from '../../backend/use-update-constituenta';
import { type IConstituenta } from '../../models/rsform';
import { generateAlias, removeAliasReference } from '../../models/rsform-api';
import { InteractionMode, useTermGraphStore } from '../../stores/term-graph';
import { RSEditContext, RSTabID } from './rsedit-context';
@ -58,13 +57,12 @@ export const RSEditState = ({
const isContentEditable = isMutable && !isArchive;
const isAttachedToOSS = schema.oss.length > 0;
const isEditor = !!user.id && schema.editors.includes(user.id);
const mode = useTermGraphStore(state => state.mode);
const [selectedCst, setSelectedCst] = useState<number[]>([]);
const [selectedEdges, setSelectedEdges] = useState<string[]>([]);
const canDeleteSelected =
(selectedCst.length > 0 && selectedCst.every(id => !schema.cstByID.get(id)?.is_inherited)) ||
(selectedEdges.length === 1 && mode === InteractionMode.edit);
(selectedCst.length === 0 && selectedEdges.length === 1);
const [focusCst, setFocusCst] = useState<IConstituenta | null>(null);
const activeCst = selectedCst.length === 0 ? null : schema.cstByID.get(selectedCst[selectedCst.length - 1])!;
@ -268,9 +266,9 @@ export const RSEditState = ({
if (!canDeleteSelected) {
return;
}
if (mode === InteractionMode.explore || selectedEdges.length === 0) {
if (selectedCst.length > 0) {
deleteSelectedCst();
} else {
} else if (selectedEdges.length === 1) {
deleteSelectedEdge();
}
}

View File

@ -72,6 +72,7 @@ interface TermGraphStore {
toggleText: () => void;
toggleClustering: () => void;
toggleGraphType: () => void;
toggleHermits: () => void;
foldHidden: boolean;
toggleFoldHidden: () => void;
@ -125,6 +126,7 @@ export const useTermGraphStore = create<TermGraphStore>()(
toggleFocusOutputs: () =>
set(state => ({ filter: { ...state.filter, focusShowOutputs: !state.filter.focusShowOutputs } })),
toggleText: () => set(state => ({ filter: { ...state.filter, noText: !state.filter.noText } })),
toggleHermits: () => set(state => ({ filter: { ...state.filter, noHermits: !state.filter.noHermits } })),
toggleClustering: () => set(state => ({ filter: { ...state.filter, foldDerived: !state.filter.foldDerived } })),
toggleGraphType: () =>
set(state => ({

View File

@ -84,11 +84,26 @@
cursor: inherit;
&.selected {
filter: drop-shadow(0 0 4px var(--color-primary));
filter: drop-shadow(0 0 4px var(--color-primary)) drop-shadow(0 0 6px var(--color-primary));
.dark & {
filter: drop-shadow(0 0 4px var(--color-accent-orange-foreground))
drop-shadow(0 0 6px var(--color-accent-orange-foreground));
}
}
.mode-edit & {
cursor: pointer;
.mode-space & {
pointer-events: none;
}
}
.rf-edge-events {
pointer-events: stroke;
stroke-width: 6;
stroke: transparent;
.mode-space & {
pointer-events: none;
}
}