mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +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'>
|
<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 />
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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'}
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user