mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Rework graph UI pt1
This commit is contained in:
parent
3ee7e110cf
commit
c1f69f23e8
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -133,6 +133,7 @@
|
|||
"компаратив",
|
||||
"конституент",
|
||||
"Конституента",
|
||||
"конституентами",
|
||||
"конституенту",
|
||||
"конституенты",
|
||||
"неинтерпретируемый",
|
||||
|
|
|
@ -23,7 +23,7 @@ function HelpTermGraph() {
|
|||
<div className='dense'>
|
||||
<h1>Клавиши</h1>
|
||||
<p><b>Клик на конституенту</b> - выделение</p>
|
||||
<p><b>Клик на выделенную</b> - редактирование</p>
|
||||
<p><b>Двойной клик</b> - редактирование</p>
|
||||
<p><b>Delete</b> - удалить выбранные</p>
|
||||
<br />
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ function SelectorButton({
|
|||
{...restProps}
|
||||
>
|
||||
{icon ? icon : null}
|
||||
{text ? <div className={'whitespace-nowrap pb-1'}>{text}</div> : null}
|
||||
{text ? <div className={'whitespace-nowrap'}>{text}</div> : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
|||
}}
|
||||
>
|
||||
<GraphUI
|
||||
animated={false}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
layoutType='hierarchicalTd'
|
||||
|
|
|
@ -13,31 +13,29 @@ import RSListToolbar from './RSListToolbar';
|
|||
import RSTable from './RSTable';
|
||||
|
||||
interface EditorRSListProps {
|
||||
selected: ConstituentaID[];
|
||||
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps) {
|
||||
function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
||||
const { calculateHeight } = useConceptOptions();
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const controller = useRSEdit();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!controller.schema || selected.length === 0) {
|
||||
if (!controller.schema || controller.selected.length === 0) {
|
||||
setRowSelection({});
|
||||
} else {
|
||||
const newRowSelection: RowSelectionState = {};
|
||||
controller.schema.items.forEach((cst, index) => {
|
||||
newRowSelection[String(index)] = selected.includes(cst.id);
|
||||
newRowSelection[String(index)] = controller.selected.includes(cst.id);
|
||||
});
|
||||
setRowSelection(newRowSelection);
|
||||
}
|
||||
}, [selected, controller.schema]);
|
||||
}, [controller.selected, controller.schema]);
|
||||
|
||||
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
||||
if (!controller.schema) {
|
||||
setSelected([]);
|
||||
controller.deselectAll();
|
||||
} else {
|
||||
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||
const newSelection: ConstituentaID[] = [];
|
||||
|
@ -46,7 +44,7 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
|
|||
newSelection.push(cst.id);
|
||||
}
|
||||
});
|
||||
setSelected(newSelection);
|
||||
controller.setSelection(newSelection);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +52,7 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
|
|||
if (!controller.isContentEditable || controller.isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Delete' && selected.length > 0) {
|
||||
if (event.key === 'Delete' && controller.selected.length > 0) {
|
||||
event.preventDefault();
|
||||
controller.deleteCst();
|
||||
return;
|
||||
|
@ -69,7 +67,7 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
|
|||
}
|
||||
|
||||
function processAltKey(code: string): boolean {
|
||||
if (selected.length > 0) {
|
||||
if (controller.selected.length > 0) {
|
||||
// prettier-ignore
|
||||
switch (code) {
|
||||
case 'ArrowUp': controller.moveUp(); return true;
|
||||
|
@ -100,12 +98,12 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
|
|||
{controller.isContentEditable ? (
|
||||
<SelectedCounter
|
||||
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||
selectedCount={selected.length}
|
||||
selectedCount={controller.selected.length}
|
||||
position='top-[0.3rem] left-2'
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{controller.isContentEditable ? <RSListToolbar selectedCount={selected.length} /> : null}
|
||||
{controller.isContentEditable ? <RSListToolbar /> : null}
|
||||
<div
|
||||
className={clsx('border-b', {
|
||||
'pt-[2.3rem]': controller.isContentEditable,
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { BiDownArrowCircle, BiDownvote, BiDuplicate, BiPlusCircle, BiTrash, BiUpvote } from 'react-icons/bi';
|
||||
|
||||
import BadgeHelp from '@/components/man/BadgeHelp';
|
||||
|
@ -17,33 +14,28 @@ import { getCstTypeShortcut, labelCstType, prepareTooltip } from '@/utils/labels
|
|||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
interface RSListToolbarProps {
|
||||
selectedCount: number;
|
||||
}
|
||||
|
||||
function RSListToolbar({ selectedCount }: RSListToolbarProps) {
|
||||
function RSListToolbar() {
|
||||
const controller = useRSEdit();
|
||||
const insertMenu = useDropdown();
|
||||
const nothingSelected = useMemo(() => selectedCount === 0, [selectedCount]);
|
||||
|
||||
return (
|
||||
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex items-start'>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
|
||||
icon={<BiUpvote size='1.25rem' className='icon-primary' />}
|
||||
disabled={controller.isProcessing || nothingSelected}
|
||||
disabled={controller.isProcessing || controller.nothingSelected}
|
||||
onClick={controller.moveUp}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
|
||||
icon={<BiDownvote size='1.25rem' className='icon-primary' />}
|
||||
disabled={controller.isProcessing || nothingSelected}
|
||||
disabled={controller.isProcessing || controller.nothingSelected}
|
||||
onClick={controller.moveDown}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}
|
||||
icon={<BiDuplicate size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing || selectedCount !== 1}
|
||||
disabled={controller.isProcessing || controller.selected.length !== 1}
|
||||
onClick={controller.cloneCst}
|
||||
/>
|
||||
<MiniButton
|
||||
|
@ -74,7 +66,7 @@ function RSListToolbar({ selectedCount }: RSListToolbarProps) {
|
|||
<MiniButton
|
||||
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
|
||||
icon={<BiTrash size='1.25rem' className='icon-red' />}
|
||||
disabled={controller.isProcessing || nothingSelected}
|
||||
disabled={controller.isProcessing || controller.nothingSelected}
|
||||
onClick={controller.deleteCst}
|
||||
/>
|
||||
<BadgeHelp topic={HelpTopic.CST_LIST} offset={5} />
|
||||
|
|
|
@ -17,6 +17,7 @@ import { colorBgGraphNode } from '@/styling/color';
|
|||
import { storage, TIMEOUT_GRAPH_REFRESH } from '@/utils/constants';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import GraphSelectors from './GraphSelectors';
|
||||
import GraphSidebar from './GraphSidebar';
|
||||
import GraphToolbar from './GraphToolbar';
|
||||
import TermGraph from './TermGraph';
|
||||
|
@ -24,12 +25,10 @@ import useGraphFilter from './useGraphFilter';
|
|||
import ViewHidden from './ViewHidden';
|
||||
|
||||
interface EditorTermGraphProps {
|
||||
selected: ConstituentaID[];
|
||||
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphProps) {
|
||||
function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||
const controller = useRSEdit();
|
||||
const { colors } = useConceptOptions();
|
||||
|
||||
|
@ -53,8 +52,6 @@ function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphP
|
|||
|
||||
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
|
||||
|
||||
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
||||
|
||||
const [layout, setLayout] = useLocalStorage<LayoutTypes>(storage.rsgraphLayout, 'treeTd2d');
|
||||
const [coloringScheme, setColoringScheme] = useLocalStorage<GraphColoringScheme>(
|
||||
storage.rsgraphColoringScheme,
|
||||
|
@ -118,33 +115,18 @@ function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphP
|
|||
return result;
|
||||
}, [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() {
|
||||
if (!controller.schema) {
|
||||
return;
|
||||
}
|
||||
const definition = selected.map(id => controller.schema!.items.find(cst => cst.id === id)!.alias).join(' ');
|
||||
controller.createCst(selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
||||
const definition = controller.selected
|
||||
.map(id => controller.schema!.items.find(cst => cst.id === id)!.alias)
|
||||
.join(' ');
|
||||
controller.createCst(controller.nothingSelected ? CstType.BASE : CstType.TERM, false, definition);
|
||||
}
|
||||
|
||||
function handleDeleteCst() {
|
||||
if (!controller.schema || selected.length === 0) {
|
||||
if (!controller.schema || controller.nothingSelected) {
|
||||
return;
|
||||
}
|
||||
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 (
|
||||
<div tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||
<AnimatePresence>
|
||||
|
@ -193,12 +206,11 @@ function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphP
|
|||
<SelectedCounter
|
||||
hideZero
|
||||
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||
selectedCount={selected.length}
|
||||
selectedCount={controller.selected.length}
|
||||
position='top-[0.3rem] left-0'
|
||||
/>
|
||||
|
||||
<GraphToolbar
|
||||
nothingSelected={nothingSelected}
|
||||
is3D={is3D}
|
||||
orbit={orbit}
|
||||
noText={filterParams.noText}
|
||||
|
@ -225,36 +237,27 @@ function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphP
|
|||
</Overlay>
|
||||
) : null}
|
||||
|
||||
<Overlay position='top-0 left-0' className='cc-column w-[13.5rem]'>
|
||||
<GraphSidebar
|
||||
coloring={coloringScheme}
|
||||
layout={layout}
|
||||
setLayout={handleChangeLayout}
|
||||
setColoring={setColoringScheme}
|
||||
/>
|
||||
<ViewHidden
|
||||
items={hidden}
|
||||
selected={selected}
|
||||
schema={controller.schema}
|
||||
coloringScheme={coloringScheme}
|
||||
toggleSelection={toggleDismissed}
|
||||
onEdit={onOpenEdit}
|
||||
/>
|
||||
<Overlay position='top-9 left-0' className='flex gap-1'>
|
||||
<div className='cc-column w-[13.5rem]'>
|
||||
<GraphSelectors
|
||||
coloring={coloringScheme}
|
||||
layout={layout}
|
||||
setLayout={handleChangeLayout}
|
||||
setColoring={setColoringScheme}
|
||||
/>
|
||||
<ViewHidden
|
||||
items={hidden}
|
||||
selected={controller.selected}
|
||||
schema={controller.schema}
|
||||
coloringScheme={coloringScheme}
|
||||
toggleSelection={controller.toggleSelect}
|
||||
onEdit={onOpenEdit}
|
||||
/>
|
||||
</div>
|
||||
<GraphSidebar />
|
||||
</Overlay>
|
||||
|
||||
<TermGraph
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
selectedIDs={selected}
|
||||
layout={layout}
|
||||
is3D={is3D}
|
||||
orbit={orbit}
|
||||
onSelect={handleGraphSelection}
|
||||
setHoverID={setHoverID}
|
||||
onEdit={onOpenEdit}
|
||||
onDeselectAll={() => setSelected([])}
|
||||
toggleResetView={toggleResetView}
|
||||
/>
|
||||
{graph}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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 { GraphColoringScheme } from '@/models/miscellaneous';
|
||||
import { mapLabelColoring, mapLabelLayout } from '@/utils/labels';
|
||||
import { SelectorGraphColoring, SelectorGraphLayout } from '@/utils/selectors';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
|
||||
interface GraphSidebarProps {
|
||||
coloring: GraphColoringScheme;
|
||||
layout: LayoutTypes;
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
setLayout: (newValue: LayoutTypes) => void;
|
||||
setColoring: (newValue: GraphColoringScheme) => void;
|
||||
}
|
||||
function GraphSidebar() {
|
||||
const controller = useRSEdit();
|
||||
|
||||
function GraphSidebar({ coloring, setColoring, layout, setLayout }: GraphSidebarProps) {
|
||||
return (
|
||||
<div className='px-2 text-sm select-none mt-9'>
|
||||
<SelectSingle
|
||||
placeholder='Выберите цвет'
|
||||
options={SelectorGraphColoring}
|
||||
isSearchable={false}
|
||||
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
|
||||
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
|
||||
<div className='flex flex-col gap-1 clr-app'>
|
||||
<MiniButton
|
||||
titleHtml='<b>Сбросить выделение</b>'
|
||||
icon={<BiReset size='1.25rem' className='icon-primary' />}
|
||||
onClick={controller.deselectAll}
|
||||
/>
|
||||
<SelectSingle
|
||||
placeholder='Способ расположения'
|
||||
options={SelectorGraphLayout}
|
||||
isSearchable={false}
|
||||
value={layout ? { value: layout, label: mapLabelLayout.get(layout) } : null}
|
||||
onChange={data => setLayout(data?.value ?? SelectorGraphLayout[0].value)}
|
||||
<MiniButton
|
||||
titleHtml='<b>Выделение базиса</b> - замыкание выделения влияющими конституентами'
|
||||
icon={<LuMinimize size='1.25rem' className='icon-primary' />}
|
||||
disabled={controller.nothingSelected}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'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 MiniButton from '@/components/ui/MiniButton';
|
||||
|
@ -10,7 +11,6 @@ import { HelpTopic } from '@/models/miscellaneous';
|
|||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
interface GraphToolbarProps {
|
||||
nothingSelected: boolean;
|
||||
is3D: boolean;
|
||||
|
||||
orbit: boolean;
|
||||
|
@ -26,7 +26,6 @@ interface GraphToolbarProps {
|
|||
}
|
||||
|
||||
function GraphToolbar({
|
||||
nothingSelected,
|
||||
is3D,
|
||||
noText,
|
||||
toggleNoText,
|
||||
|
@ -40,7 +39,7 @@ function GraphToolbar({
|
|||
const controller = useRSEdit();
|
||||
|
||||
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
|
||||
title='Настройки фильтрации узлов и связей'
|
||||
icon={<BiFilterAlt size='1.25rem' className='icon-primary' />}
|
||||
|
@ -58,7 +57,7 @@ function GraphToolbar({
|
|||
onClick={toggleNoText}
|
||||
/>
|
||||
<MiniButton
|
||||
icon={<BiCollapse size='1.25rem' className='icon-primary' />}
|
||||
icon={<LuImage size='1.25rem' className='icon-primary' />}
|
||||
title='Восстановить камеру'
|
||||
onClick={onResetViewpoint}
|
||||
/>
|
||||
|
@ -80,7 +79,7 @@ function GraphToolbar({
|
|||
<MiniButton
|
||||
title='Удалить выбранные'
|
||||
icon={<BiTrash size='1.25rem' className='icon-red' />}
|
||||
disabled={nothingSelected || controller.isProcessing}
|
||||
disabled={controller.nothingSelected || controller.isProcessing}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -20,7 +20,7 @@ interface TermGraphProps {
|
|||
setHoverID: (newID: ConstituentaID | undefined) => void;
|
||||
onEdit: (cstID: ConstituentaID) => void;
|
||||
onSelect: (newID: ConstituentaID) => void;
|
||||
onDeselectAll: () => void;
|
||||
onDeselect: (newID: ConstituentaID) => void;
|
||||
|
||||
toggleResetView: boolean;
|
||||
}
|
||||
|
@ -38,54 +38,45 @@ function TermGraph({
|
|||
setHoverID,
|
||||
onEdit,
|
||||
onSelect,
|
||||
onDeselectAll
|
||||
onDeselect
|
||||
}: TermGraphProps) {
|
||||
const { calculateHeight, darkMode } = useConceptOptions();
|
||||
const graphRef = useRef<GraphCanvasRef | null>(null);
|
||||
|
||||
const { selections, actives, setSelections, onCanvasClick, onNodePointerOver, onNodePointerOut } = useSelection({
|
||||
const { selections, setSelections } = useSelection({
|
||||
ref: graphRef,
|
||||
nodes,
|
||||
edges,
|
||||
type: 'multi', // 'single' | 'multi' | 'multiModifier'
|
||||
pathSelectionType: 'out',
|
||||
pathHoverType: 'all',
|
||||
focusOnSelect: false
|
||||
type: 'multi'
|
||||
});
|
||||
|
||||
const handleHoverIn = useCallback(
|
||||
(node: GraphNode) => {
|
||||
setHoverID(Number(node.id));
|
||||
if (onNodePointerOver) onNodePointerOver(node);
|
||||
},
|
||||
[onNodePointerOver, setHoverID]
|
||||
[setHoverID]
|
||||
);
|
||||
|
||||
const handleHoverOut = useCallback(
|
||||
(node: GraphNode) => {
|
||||
setHoverID(undefined);
|
||||
if (onNodePointerOut) onNodePointerOut(node);
|
||||
},
|
||||
[onNodePointerOut, setHoverID]
|
||||
);
|
||||
const handleHoverOut = useCallback(() => {
|
||||
setHoverID(undefined);
|
||||
}, [setHoverID]);
|
||||
|
||||
const handleNodeClick = useCallback(
|
||||
(node: GraphNode) => {
|
||||
if (selections.includes(node.id)) {
|
||||
onEdit(Number(node.id));
|
||||
onDeselect(Number(node.id));
|
||||
} else {
|
||||
onSelect(Number(node.id));
|
||||
}
|
||||
},
|
||||
[onSelect, selections, onEdit]
|
||||
[onSelect, selections, onDeselect]
|
||||
);
|
||||
|
||||
const handleCanvasClick = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
onDeselectAll();
|
||||
if (onCanvasClick) onCanvasClick(event);
|
||||
const handleNodeDoubleClick = useCallback(
|
||||
(node: GraphNode) => {
|
||||
onEdit(Number(node.id));
|
||||
},
|
||||
[onCanvasClick, onDeselectAll]
|
||||
[onEdit]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
@ -108,15 +99,15 @@ function TermGraph({
|
|||
<div className='outline-none'>
|
||||
<div className='relative' style={{ width: canvasWidth, height: canvasHeight }}>
|
||||
<GraphUI
|
||||
draggable
|
||||
ref={graphRef}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
ref={graphRef}
|
||||
animated={false}
|
||||
draggable
|
||||
layoutType={layout}
|
||||
selections={selections}
|
||||
actives={actives}
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
onNodeClick={handleNodeClick}
|
||||
onCanvasClick={handleCanvasClick}
|
||||
onNodePointerOver={handleHoverIn}
|
||||
onNodePointerOut={handleHoverOut}
|
||||
cameraMode={orbit ? 'orbit' : is3D ? 'rotate' : 'pan'}
|
||||
|
|
|
@ -48,12 +48,23 @@ import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
|||
|
||||
interface IRSEditContext {
|
||||
schema?: IRSForm;
|
||||
selected: ConstituentaID[];
|
||||
|
||||
isMutable: boolean;
|
||||
isContentEditable: boolean;
|
||||
isProcessing: 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;
|
||||
createVersion: () => void;
|
||||
editVersions: () => void;
|
||||
|
||||
moveUp: () => void;
|
||||
moveDown: () => void;
|
||||
|
@ -70,13 +81,11 @@ interface IRSEditContext {
|
|||
share: () => void;
|
||||
toggleSubscribe: () => void;
|
||||
download: () => void;
|
||||
|
||||
reindex: () => void;
|
||||
produceStructure: () => void;
|
||||
inlineSynthesis: () => void;
|
||||
substitute: () => void;
|
||||
|
||||
createVersion: () => void;
|
||||
editVersions: () => void;
|
||||
}
|
||||
|
||||
const RSEditContext = createContext<IRSEditContext | null>(null);
|
||||
|
@ -119,6 +128,7 @@ export const RSEditState = ({
|
|||
);
|
||||
}, [user?.is_staff, mode, model.isOwned]);
|
||||
const isContentEditable = useMemo(() => isMutable && !model.isArchive, [isMutable, model.isArchive]);
|
||||
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
||||
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [showClone, setShowClone] = useState(false);
|
||||
|
@ -464,12 +474,23 @@ export const RSEditState = ({
|
|||
<RSEditContext.Provider
|
||||
value={{
|
||||
schema: model.schema,
|
||||
selected,
|
||||
isMutable,
|
||||
isContentEditable,
|
||||
isProcessing: model.processing,
|
||||
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,
|
||||
createVersion: () => setShowCreateVersion(true),
|
||||
editVersions: () => setShowEditVersions(true),
|
||||
|
||||
moveUp,
|
||||
moveDown,
|
||||
|
@ -486,13 +507,11 @@ export const RSEditState = ({
|
|||
claim,
|
||||
share,
|
||||
toggleSubscribe,
|
||||
|
||||
reindex,
|
||||
inlineSynthesis: () => setShowInlineSynthesis(true),
|
||||
produceStructure,
|
||||
substitute,
|
||||
|
||||
createVersion: () => setShowCreateVersion(true),
|
||||
editVersions: () => setShowEditVersions(true)
|
||||
substitute
|
||||
}}
|
||||
>
|
||||
{model.schema ? (
|
||||
|
|
|
@ -204,7 +204,7 @@ function RSTabs() {
|
|||
</TabPanel>
|
||||
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '' : 'none' }}>
|
||||
<EditorRSList selected={selected} setSelected={setSelected} onOpenEdit={onOpenCst} />
|
||||
<EditorRSList onOpenEdit={onOpenCst} />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '' : 'none' }}>
|
||||
|
@ -217,7 +217,7 @@ function RSTabs() {
|
|||
</TabPanel>
|
||||
|
||||
<TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '' : 'none' }}>
|
||||
<EditorTermGraph selected={selected} setSelected={setSelected} onOpenEdit={onOpenCst} />
|
||||
<EditorTermGraph onOpenEdit={onOpenCst} />
|
||||
</TabPanel>
|
||||
</AnimateFade>
|
||||
</Tabs>
|
||||
|
|
|
@ -179,7 +179,7 @@ export const graphLightT = {
|
|||
activeFill: '#1DE9AC',
|
||||
opacity: 1,
|
||||
selectedOpacity: 1,
|
||||
inactiveOpacity: 0.2,
|
||||
inactiveOpacity: 0.5,
|
||||
label: {
|
||||
color: '#2A6475',
|
||||
stroke: '#fff',
|
||||
|
@ -231,7 +231,7 @@ export const graphDarkT = {
|
|||
activeFill: '#1DE9AC',
|
||||
opacity: 1,
|
||||
selectedOpacity: 1,
|
||||
inactiveOpacity: 0.2,
|
||||
inactiveOpacity: 0.5,
|
||||
label: {
|
||||
stroke: '#1E2026',
|
||||
color: '#ACBAC7',
|
||||
|
|
Loading…
Reference in New Issue
Block a user