Rework graph UI pt1

This commit is contained in:
IRBorisov 2024-04-03 15:51:57 +03:00
parent 3ee7e110cf
commit c1f69f23e8
14 changed files with 202 additions and 151 deletions

View File

@ -133,6 +133,7 @@
"компаратив", "компаратив",
"конституент", "конституент",
"Конституента", "Конституента",
"конституентами",
"конституенту", "конституенту",
"конституенты", "конституенты",
"неинтерпретируемый", "неинтерпретируемый",

View File

@ -23,7 +23,7 @@ function HelpTermGraph() {
<div className='dense'> <div className='dense'>
<h1>Клавиши</h1> <h1>Клавиши</h1>
<p><b>Клик на конституенту</b> - выделение</p> <p><b>Клик на конституенту</b> - выделение</p>
<p><b>Клик на выделенную</b> - редактирование</p> <p><b>Двойной клик</b> - редактирование</p>
<p><b>Delete</b> - удалить выбранные</p> <p><b>Delete</b> - удалить выбранные</p>
<br /> <br />

View File

@ -45,7 +45,7 @@ function SelectorButton({
{...restProps} {...restProps}
> >
{icon ? icon : null} {icon ? icon : null}
{text ? <div className={'whitespace-nowrap pb-1'}>{text}</div> : null} {text ? <div className={'whitespace-nowrap'}>{text}</div> : null}
</button> </button>
); );
} }

View File

@ -69,6 +69,7 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
}} }}
> >
<GraphUI <GraphUI
animated={false}
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
layoutType='hierarchicalTd' layoutType='hierarchicalTd'

View File

@ -13,31 +13,29 @@ import RSListToolbar from './RSListToolbar';
import RSTable from './RSTable'; import RSTable from './RSTable';
interface EditorRSListProps { interface EditorRSListProps {
selected: ConstituentaID[];
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
onOpenEdit: (cstID: ConstituentaID) => void; onOpenEdit: (cstID: ConstituentaID) => void;
} }
function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps) { function EditorRSList({ onOpenEdit }: EditorRSListProps) {
const { calculateHeight } = useConceptOptions(); const { calculateHeight } = useConceptOptions();
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const controller = useRSEdit(); const controller = useRSEdit();
useLayoutEffect(() => { useLayoutEffect(() => {
if (!controller.schema || selected.length === 0) { if (!controller.schema || controller.selected.length === 0) {
setRowSelection({}); setRowSelection({});
} else { } else {
const newRowSelection: RowSelectionState = {}; const newRowSelection: RowSelectionState = {};
controller.schema.items.forEach((cst, index) => { controller.schema.items.forEach((cst, index) => {
newRowSelection[String(index)] = selected.includes(cst.id); newRowSelection[String(index)] = controller.selected.includes(cst.id);
}); });
setRowSelection(newRowSelection); setRowSelection(newRowSelection);
} }
}, [selected, controller.schema]); }, [controller.selected, controller.schema]);
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) { function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
if (!controller.schema) { if (!controller.schema) {
setSelected([]); controller.deselectAll();
} else { } else {
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater; const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
const newSelection: ConstituentaID[] = []; const newSelection: ConstituentaID[] = [];
@ -46,7 +44,7 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
newSelection.push(cst.id); newSelection.push(cst.id);
} }
}); });
setSelected(newSelection); controller.setSelection(newSelection);
} }
} }
@ -54,7 +52,7 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
if (!controller.isContentEditable || controller.isProcessing) { if (!controller.isContentEditable || controller.isProcessing) {
return; return;
} }
if (event.key === 'Delete' && selected.length > 0) { if (event.key === 'Delete' && controller.selected.length > 0) {
event.preventDefault(); event.preventDefault();
controller.deleteCst(); controller.deleteCst();
return; return;
@ -69,7 +67,7 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
} }
function processAltKey(code: string): boolean { function processAltKey(code: string): boolean {
if (selected.length > 0) { if (controller.selected.length > 0) {
// prettier-ignore // prettier-ignore
switch (code) { switch (code) {
case 'ArrowUp': controller.moveUp(); return true; case 'ArrowUp': controller.moveUp(); return true;
@ -100,12 +98,12 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
{controller.isContentEditable ? ( {controller.isContentEditable ? (
<SelectedCounter <SelectedCounter
totalCount={controller.schema?.stats?.count_all ?? 0} totalCount={controller.schema?.stats?.count_all ?? 0}
selectedCount={selected.length} selectedCount={controller.selected.length}
position='top-[0.3rem] left-2' position='top-[0.3rem] left-2'
/> />
) : null} ) : null}
{controller.isContentEditable ? <RSListToolbar selectedCount={selected.length} /> : null} {controller.isContentEditable ? <RSListToolbar /> : null}
<div <div
className={clsx('border-b', { className={clsx('border-b', {
'pt-[2.3rem]': controller.isContentEditable, 'pt-[2.3rem]': controller.isContentEditable,

View File

@ -1,6 +1,3 @@
'use client';
import { useMemo } from 'react';
import { BiDownArrowCircle, BiDownvote, BiDuplicate, BiPlusCircle, BiTrash, BiUpvote } from 'react-icons/bi'; import { BiDownArrowCircle, BiDownvote, BiDuplicate, BiPlusCircle, BiTrash, BiUpvote } from 'react-icons/bi';
import BadgeHelp from '@/components/man/BadgeHelp'; import BadgeHelp from '@/components/man/BadgeHelp';
@ -17,33 +14,28 @@ import { getCstTypeShortcut, labelCstType, prepareTooltip } from '@/utils/labels
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
interface RSListToolbarProps { function RSListToolbar() {
selectedCount: number;
}
function RSListToolbar({ selectedCount }: RSListToolbarProps) {
const controller = useRSEdit(); const controller = useRSEdit();
const insertMenu = useDropdown(); const insertMenu = useDropdown();
const nothingSelected = useMemo(() => selectedCount === 0, [selectedCount]);
return ( return (
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex items-start'> <Overlay position='top-1 right-1/2 translate-x-1/2' className='flex items-start'>
<MiniButton <MiniButton
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')} titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
icon={<BiUpvote size='1.25rem' className='icon-primary' />} icon={<BiUpvote size='1.25rem' className='icon-primary' />}
disabled={controller.isProcessing || nothingSelected} disabled={controller.isProcessing || controller.nothingSelected}
onClick={controller.moveUp} onClick={controller.moveUp}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')} titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
icon={<BiDownvote size='1.25rem' className='icon-primary' />} icon={<BiDownvote size='1.25rem' className='icon-primary' />}
disabled={controller.isProcessing || nothingSelected} disabled={controller.isProcessing || controller.nothingSelected}
onClick={controller.moveDown} onClick={controller.moveDown}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')} titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}
icon={<BiDuplicate size='1.25rem' className='icon-green' />} icon={<BiDuplicate size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing || selectedCount !== 1} disabled={controller.isProcessing || controller.selected.length !== 1}
onClick={controller.cloneCst} onClick={controller.cloneCst}
/> />
<MiniButton <MiniButton
@ -74,7 +66,7 @@ function RSListToolbar({ selectedCount }: RSListToolbarProps) {
<MiniButton <MiniButton
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')} titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
icon={<BiTrash size='1.25rem' className='icon-red' />} icon={<BiTrash size='1.25rem' className='icon-red' />}
disabled={controller.isProcessing || nothingSelected} disabled={controller.isProcessing || controller.nothingSelected}
onClick={controller.deleteCst} onClick={controller.deleteCst}
/> />
<BadgeHelp topic={HelpTopic.CST_LIST} offset={5} /> <BadgeHelp topic={HelpTopic.CST_LIST} offset={5} />

View File

@ -17,6 +17,7 @@ import { colorBgGraphNode } from '@/styling/color';
import { storage, TIMEOUT_GRAPH_REFRESH } from '@/utils/constants'; import { storage, TIMEOUT_GRAPH_REFRESH } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
import GraphSelectors from './GraphSelectors';
import GraphSidebar from './GraphSidebar'; import GraphSidebar from './GraphSidebar';
import GraphToolbar from './GraphToolbar'; import GraphToolbar from './GraphToolbar';
import TermGraph from './TermGraph'; import TermGraph from './TermGraph';
@ -24,12 +25,10 @@ import useGraphFilter from './useGraphFilter';
import ViewHidden from './ViewHidden'; import ViewHidden from './ViewHidden';
interface EditorTermGraphProps { interface EditorTermGraphProps {
selected: ConstituentaID[];
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
onOpenEdit: (cstID: ConstituentaID) => void; onOpenEdit: (cstID: ConstituentaID) => void;
} }
function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphProps) { function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
const controller = useRSEdit(); const controller = useRSEdit();
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
@ -53,8 +52,6 @@ function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphP
const [hidden, setHidden] = useState<ConstituentaID[]>([]); const [hidden, setHidden] = useState<ConstituentaID[]>([]);
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
const [layout, setLayout] = useLocalStorage<LayoutTypes>(storage.rsgraphLayout, 'treeTd2d'); const [layout, setLayout] = useLocalStorage<LayoutTypes>(storage.rsgraphLayout, 'treeTd2d');
const [coloringScheme, setColoringScheme] = useLocalStorage<GraphColoringScheme>( const [coloringScheme, setColoringScheme] = useLocalStorage<GraphColoringScheme>(
storage.rsgraphColoringScheme, storage.rsgraphColoringScheme,
@ -118,33 +115,18 @@ function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphP
return result; return result;
}, [filtered.nodes]); }, [filtered.nodes]);
const handleGraphSelection = useCallback(
(newID: ConstituentaID) => {
setSelected(prev => [...prev, newID]);
},
[setSelected]
);
function toggleDismissed(cstID: ConstituentaID) {
setSelected(prev => {
if (prev.includes(cstID)) {
return [...prev.filter(id => id !== cstID)];
} else {
return [...prev, cstID];
}
});
}
function handleCreateCst() { function handleCreateCst() {
if (!controller.schema) { if (!controller.schema) {
return; return;
} }
const definition = selected.map(id => controller.schema!.items.find(cst => cst.id === id)!.alias).join(' '); const definition = controller.selected
controller.createCst(selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition); .map(id => controller.schema!.items.find(cst => cst.id === id)!.alias)
.join(' ');
controller.createCst(controller.nothingSelected ? CstType.BASE : CstType.TERM, false, definition);
} }
function handleDeleteCst() { function handleDeleteCst() {
if (!controller.schema || selected.length === 0) { if (!controller.schema || controller.nothingSelected) {
return; return;
} }
controller.deleteCst(); controller.deleteCst();
@ -178,6 +160,37 @@ function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphP
} }
} }
const graph = useMemo(
() => (
<TermGraph
nodes={nodes}
edges={edges}
selectedIDs={controller.selected}
layout={layout}
is3D={is3D}
orbit={orbit}
onSelect={controller.select}
onDeselect={controller.deselect}
setHoverID={setHoverID}
onEdit={onOpenEdit}
toggleResetView={toggleResetView}
/>
),
[
edges,
nodes,
controller.selected,
layout,
is3D,
orbit,
setHoverID,
onOpenEdit,
toggleResetView,
controller.select,
controller.deselect
]
);
return ( return (
<div tabIndex={-1} onKeyDown={handleKeyDown}> <div tabIndex={-1} onKeyDown={handleKeyDown}>
<AnimatePresence> <AnimatePresence>
@ -193,12 +206,11 @@ function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphP
<SelectedCounter <SelectedCounter
hideZero hideZero
totalCount={controller.schema?.stats?.count_all ?? 0} totalCount={controller.schema?.stats?.count_all ?? 0}
selectedCount={selected.length} selectedCount={controller.selected.length}
position='top-[0.3rem] left-0' position='top-[0.3rem] left-0'
/> />
<GraphToolbar <GraphToolbar
nothingSelected={nothingSelected}
is3D={is3D} is3D={is3D}
orbit={orbit} orbit={orbit}
noText={filterParams.noText} noText={filterParams.noText}
@ -225,36 +237,27 @@ function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphP
</Overlay> </Overlay>
) : null} ) : null}
<Overlay position='top-0 left-0' className='cc-column w-[13.5rem]'> <Overlay position='top-9 left-0' className='flex gap-1'>
<GraphSidebar <div className='cc-column w-[13.5rem]'>
coloring={coloringScheme} <GraphSelectors
layout={layout} coloring={coloringScheme}
setLayout={handleChangeLayout} layout={layout}
setColoring={setColoringScheme} setLayout={handleChangeLayout}
/> setColoring={setColoringScheme}
<ViewHidden />
items={hidden} <ViewHidden
selected={selected} items={hidden}
schema={controller.schema} selected={controller.selected}
coloringScheme={coloringScheme} schema={controller.schema}
toggleSelection={toggleDismissed} coloringScheme={coloringScheme}
onEdit={onOpenEdit} toggleSelection={controller.toggleSelect}
/> onEdit={onOpenEdit}
/>
</div>
<GraphSidebar />
</Overlay> </Overlay>
<TermGraph {graph}
nodes={nodes}
edges={edges}
selectedIDs={selected}
layout={layout}
is3D={is3D}
orbit={orbit}
onSelect={handleGraphSelection}
setHoverID={setHoverID}
onEdit={onOpenEdit}
onDeselectAll={() => setSelected([])}
toggleResetView={toggleResetView}
/>
</div> </div>
); );
} }

View File

@ -0,0 +1,37 @@
import { LayoutTypes } from 'reagraph';
import SelectSingle from '@/components/ui/SelectSingle';
import { GraphColoringScheme } from '@/models/miscellaneous';
import { mapLabelColoring, mapLabelLayout } from '@/utils/labels';
import { SelectorGraphColoring, SelectorGraphLayout } from '@/utils/selectors';
interface GraphSelectorsProps {
coloring: GraphColoringScheme;
layout: LayoutTypes;
setLayout: (newValue: LayoutTypes) => void;
setColoring: (newValue: GraphColoringScheme) => void;
}
function GraphSelectors({ coloring, setColoring, layout, setLayout }: GraphSelectorsProps) {
return (
<div className='px-2 text-sm select-none'>
<SelectSingle
placeholder='Выберите цвет'
options={SelectorGraphColoring}
isSearchable={false}
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
/>
<SelectSingle
placeholder='Способ расположения'
options={SelectorGraphLayout}
isSearchable={false}
value={layout ? { value: layout, label: mapLabelLayout.get(layout) } : null}
onChange={data => setLayout(data?.value ?? SelectorGraphLayout[0].value)}
/>
</div>
);
}
export default GraphSelectors;

View File

@ -1,34 +1,44 @@
import { LayoutTypes } from 'reagraph'; import { BiGitBranch, BiGitMerge, BiReset } from 'react-icons/bi';
import { LuExpand, LuMaximize, LuMinimize } from 'react-icons/lu';
import SelectSingle from '@/components/ui/SelectSingle'; import MiniButton from '@/components/ui/MiniButton';
import { GraphColoringScheme } from '@/models/miscellaneous';
import { mapLabelColoring, mapLabelLayout } from '@/utils/labels';
import { SelectorGraphColoring, SelectorGraphLayout } from '@/utils/selectors';
interface GraphSidebarProps { import { useRSEdit } from '../RSEditContext';
coloring: GraphColoringScheme;
layout: LayoutTypes;
setLayout: (newValue: LayoutTypes) => void; function GraphSidebar() {
setColoring: (newValue: GraphColoringScheme) => void; const controller = useRSEdit();
}
function GraphSidebar({ coloring, setColoring, layout, setLayout }: GraphSidebarProps) {
return ( return (
<div className='px-2 text-sm select-none mt-9'> <div className='flex flex-col gap-1 clr-app'>
<SelectSingle <MiniButton
placeholder='Выберите цвет' titleHtml='<b>Сбросить выделение</b>'
options={SelectorGraphColoring} icon={<BiReset size='1.25rem' className='icon-primary' />}
isSearchable={false} onClick={controller.deselectAll}
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
/> />
<SelectSingle <MiniButton
placeholder='Способ расположения' titleHtml='<b>Выделение базиса</b> - замыкание выделения влияющими конституентами'
options={SelectorGraphLayout} icon={<LuMinimize size='1.25rem' className='icon-primary' />}
isSearchable={false} disabled={controller.nothingSelected}
value={layout ? { value: layout, label: mapLabelLayout.get(layout) } : null} />
onChange={data => setLayout(data?.value ?? SelectorGraphLayout[0].value)} <MiniButton
titleHtml='<b>Максимизация части</b> - замыкание выделения конституентами, зависимыми только от выделенных'
icon={<LuMaximize size='1.25rem' className='icon-primary' />}
disabled={controller.nothingSelected}
/>
<MiniButton
title='Выделить все зависимые'
icon={<LuExpand size='1.25rem' className='icon-primary' />}
disabled={controller.nothingSelected}
/>
<MiniButton
title='Выделить поставщиков'
icon={<BiGitBranch size='1.25rem' className='icon-primary' />}
disabled={controller.nothingSelected}
/>
<MiniButton
title='Выделить потребителей'
icon={<BiGitMerge size='1.25rem' className='icon-primary' />}
disabled={controller.nothingSelected}
/> />
</div> </div>
); );

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { BiCollapse, BiFilterAlt, BiFont, BiFontFamily, BiPlanet, BiPlusCircle, BiTrash } from 'react-icons/bi'; import { BiFilterAlt, BiFont, BiFontFamily, BiPlanet, BiPlusCircle, BiTrash } from 'react-icons/bi';
import { LuImage } from 'react-icons/lu';
import BadgeHelp from '@/components/man/BadgeHelp'; import BadgeHelp from '@/components/man/BadgeHelp';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
@ -10,7 +11,6 @@ import { HelpTopic } from '@/models/miscellaneous';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
interface GraphToolbarProps { interface GraphToolbarProps {
nothingSelected: boolean;
is3D: boolean; is3D: boolean;
orbit: boolean; orbit: boolean;
@ -26,7 +26,6 @@ interface GraphToolbarProps {
} }
function GraphToolbar({ function GraphToolbar({
nothingSelected,
is3D, is3D,
noText, noText,
toggleNoText, toggleNoText,
@ -40,7 +39,7 @@ function GraphToolbar({
const controller = useRSEdit(); const controller = useRSEdit();
return ( return (
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'> <Overlay position='top-0 pt-1 right-1/2 translate-x-1/2 clr-app' className='flex'>
<MiniButton <MiniButton
title='Настройки фильтрации узлов и связей' title='Настройки фильтрации узлов и связей'
icon={<BiFilterAlt size='1.25rem' className='icon-primary' />} icon={<BiFilterAlt size='1.25rem' className='icon-primary' />}
@ -58,7 +57,7 @@ function GraphToolbar({
onClick={toggleNoText} onClick={toggleNoText}
/> />
<MiniButton <MiniButton
icon={<BiCollapse size='1.25rem' className='icon-primary' />} icon={<LuImage size='1.25rem' className='icon-primary' />}
title='Восстановить камеру' title='Восстановить камеру'
onClick={onResetViewpoint} onClick={onResetViewpoint}
/> />
@ -80,7 +79,7 @@ function GraphToolbar({
<MiniButton <MiniButton
title='Удалить выбранные' title='Удалить выбранные'
icon={<BiTrash size='1.25rem' className='icon-red' />} icon={<BiTrash size='1.25rem' className='icon-red' />}
disabled={nothingSelected || controller.isProcessing} disabled={controller.nothingSelected || controller.isProcessing}
onClick={onDelete} onClick={onDelete}
/> />
) : null} ) : null}

View File

@ -20,7 +20,7 @@ interface TermGraphProps {
setHoverID: (newID: ConstituentaID | undefined) => void; setHoverID: (newID: ConstituentaID | undefined) => void;
onEdit: (cstID: ConstituentaID) => void; onEdit: (cstID: ConstituentaID) => void;
onSelect: (newID: ConstituentaID) => void; onSelect: (newID: ConstituentaID) => void;
onDeselectAll: () => void; onDeselect: (newID: ConstituentaID) => void;
toggleResetView: boolean; toggleResetView: boolean;
} }
@ -38,54 +38,45 @@ function TermGraph({
setHoverID, setHoverID,
onEdit, onEdit,
onSelect, onSelect,
onDeselectAll onDeselect
}: TermGraphProps) { }: TermGraphProps) {
const { calculateHeight, darkMode } = useConceptOptions(); const { calculateHeight, darkMode } = useConceptOptions();
const graphRef = useRef<GraphCanvasRef | null>(null); const graphRef = useRef<GraphCanvasRef | null>(null);
const { selections, actives, setSelections, onCanvasClick, onNodePointerOver, onNodePointerOut } = useSelection({ const { selections, setSelections } = useSelection({
ref: graphRef, ref: graphRef,
nodes, nodes,
edges, edges,
type: 'multi', // 'single' | 'multi' | 'multiModifier' type: 'multi'
pathSelectionType: 'out',
pathHoverType: 'all',
focusOnSelect: false
}); });
const handleHoverIn = useCallback( const handleHoverIn = useCallback(
(node: GraphNode) => { (node: GraphNode) => {
setHoverID(Number(node.id)); setHoverID(Number(node.id));
if (onNodePointerOver) onNodePointerOver(node);
}, },
[onNodePointerOver, setHoverID] [setHoverID]
); );
const handleHoverOut = useCallback( const handleHoverOut = useCallback(() => {
(node: GraphNode) => { setHoverID(undefined);
setHoverID(undefined); }, [setHoverID]);
if (onNodePointerOut) onNodePointerOut(node);
},
[onNodePointerOut, setHoverID]
);
const handleNodeClick = useCallback( const handleNodeClick = useCallback(
(node: GraphNode) => { (node: GraphNode) => {
if (selections.includes(node.id)) { if (selections.includes(node.id)) {
onEdit(Number(node.id)); onDeselect(Number(node.id));
} else { } else {
onSelect(Number(node.id)); onSelect(Number(node.id));
} }
}, },
[onSelect, selections, onEdit] [onSelect, selections, onDeselect]
); );
const handleCanvasClick = useCallback( const handleNodeDoubleClick = useCallback(
(event: MouseEvent) => { (node: GraphNode) => {
onDeselectAll(); onEdit(Number(node.id));
if (onCanvasClick) onCanvasClick(event);
}, },
[onCanvasClick, onDeselectAll] [onEdit]
); );
useLayoutEffect(() => { useLayoutEffect(() => {
@ -108,15 +99,15 @@ function TermGraph({
<div className='outline-none'> <div className='outline-none'>
<div className='relative' style={{ width: canvasWidth, height: canvasHeight }}> <div className='relative' style={{ width: canvasWidth, height: canvasHeight }}>
<GraphUI <GraphUI
draggable
ref={graphRef}
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
ref={graphRef}
animated={false}
draggable
layoutType={layout} layoutType={layout}
selections={selections} selections={selections}
actives={actives} onNodeDoubleClick={handleNodeDoubleClick}
onNodeClick={handleNodeClick} onNodeClick={handleNodeClick}
onCanvasClick={handleCanvasClick}
onNodePointerOver={handleHoverIn} onNodePointerOver={handleHoverIn}
onNodePointerOut={handleHoverOut} onNodePointerOut={handleHoverOut}
cameraMode={orbit ? 'orbit' : is3D ? 'rotate' : 'pan'} cameraMode={orbit ? 'orbit' : is3D ? 'rotate' : 'pan'}

View File

@ -48,12 +48,23 @@ import { EXTEOR_TRS_FILE } from '@/utils/constants';
interface IRSEditContext { interface IRSEditContext {
schema?: IRSForm; schema?: IRSForm;
selected: ConstituentaID[];
isMutable: boolean; isMutable: boolean;
isContentEditable: boolean; isContentEditable: boolean;
isProcessing: boolean; isProcessing: boolean;
canProduceStructure: boolean; canProduceStructure: boolean;
nothingSelected: boolean;
setSelection: (selected: ConstituentaID[]) => void;
select: (target: ConstituentaID) => void;
deselect: (target: ConstituentaID) => void;
toggleSelect: (target: ConstituentaID) => void;
deselectAll: () => void;
viewVersion: (version?: number) => void; viewVersion: (version?: number) => void;
createVersion: () => void;
editVersions: () => void;
moveUp: () => void; moveUp: () => void;
moveDown: () => void; moveDown: () => void;
@ -70,13 +81,11 @@ interface IRSEditContext {
share: () => void; share: () => void;
toggleSubscribe: () => void; toggleSubscribe: () => void;
download: () => void; download: () => void;
reindex: () => void; reindex: () => void;
produceStructure: () => void; produceStructure: () => void;
inlineSynthesis: () => void; inlineSynthesis: () => void;
substitute: () => void; substitute: () => void;
createVersion: () => void;
editVersions: () => void;
} }
const RSEditContext = createContext<IRSEditContext | null>(null); const RSEditContext = createContext<IRSEditContext | null>(null);
@ -119,6 +128,7 @@ export const RSEditState = ({
); );
}, [user?.is_staff, mode, model.isOwned]); }, [user?.is_staff, mode, model.isOwned]);
const isContentEditable = useMemo(() => isMutable && !model.isArchive, [isMutable, model.isArchive]); const isContentEditable = useMemo(() => isMutable && !model.isArchive, [isMutable, model.isArchive]);
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
const [showUpload, setShowUpload] = useState(false); const [showUpload, setShowUpload] = useState(false);
const [showClone, setShowClone] = useState(false); const [showClone, setShowClone] = useState(false);
@ -464,12 +474,23 @@ export const RSEditState = ({
<RSEditContext.Provider <RSEditContext.Provider
value={{ value={{
schema: model.schema, schema: model.schema,
selected,
isMutable, isMutable,
isContentEditable, isContentEditable,
isProcessing: model.processing, isProcessing: model.processing,
canProduceStructure, canProduceStructure,
nothingSelected,
setSelection: (selected: ConstituentaID[]) => setSelected(selected),
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
toggleSelect: (target: ConstituentaID) =>
setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
deselectAll: () => setSelected([]),
viewVersion, viewVersion,
createVersion: () => setShowCreateVersion(true),
editVersions: () => setShowEditVersions(true),
moveUp, moveUp,
moveDown, moveDown,
@ -486,13 +507,11 @@ export const RSEditState = ({
claim, claim,
share, share,
toggleSubscribe, toggleSubscribe,
reindex, reindex,
inlineSynthesis: () => setShowInlineSynthesis(true), inlineSynthesis: () => setShowInlineSynthesis(true),
produceStructure, produceStructure,
substitute, substitute
createVersion: () => setShowCreateVersion(true),
editVersions: () => setShowEditVersions(true)
}} }}
> >
{model.schema ? ( {model.schema ? (

View File

@ -204,7 +204,7 @@ function RSTabs() {
</TabPanel> </TabPanel>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '' : 'none' }}> <TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '' : 'none' }}>
<EditorRSList selected={selected} setSelected={setSelected} onOpenEdit={onOpenCst} /> <EditorRSList onOpenEdit={onOpenCst} />
</TabPanel> </TabPanel>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '' : 'none' }}> <TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '' : 'none' }}>
@ -217,7 +217,7 @@ function RSTabs() {
</TabPanel> </TabPanel>
<TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '' : 'none' }}> <TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '' : 'none' }}>
<EditorTermGraph selected={selected} setSelected={setSelected} onOpenEdit={onOpenCst} /> <EditorTermGraph onOpenEdit={onOpenCst} />
</TabPanel> </TabPanel>
</AnimateFade> </AnimateFade>
</Tabs> </Tabs>

View File

@ -179,7 +179,7 @@ export const graphLightT = {
activeFill: '#1DE9AC', activeFill: '#1DE9AC',
opacity: 1, opacity: 1,
selectedOpacity: 1, selectedOpacity: 1,
inactiveOpacity: 0.2, inactiveOpacity: 0.5,
label: { label: {
color: '#2A6475', color: '#2A6475',
stroke: '#fff', stroke: '#fff',
@ -231,7 +231,7 @@ export const graphDarkT = {
activeFill: '#1DE9AC', activeFill: '#1DE9AC',
opacity: 1, opacity: 1,
selectedOpacity: 1, selectedOpacity: 1,
inactiveOpacity: 0.2, inactiveOpacity: 0.5,
label: { label: {
stroke: '#1E2026', stroke: '#1E2026',
color: '#ACBAC7', color: '#ACBAC7',