mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-14 12:50:37 +03:00
Improve graph UI
This commit is contained in:
parent
2f98ae90ff
commit
64ebce3082
28
rsconcept/frontend/package-lock.json
generated
28
rsconcept/frontend/package-lock.json
generated
|
@ -28,7 +28,7 @@
|
|||
"react-tabs": "^6.0.2",
|
||||
"react-toastify": "^9.1.3",
|
||||
"react-tooltip": "^5.26.3",
|
||||
"reagraph": "^4.15.26"
|
||||
"reagraph": "^4.15.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.7.0",
|
||||
|
@ -8301,25 +8301,13 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path2d": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path2d/-/path2d-0.1.1.tgz",
|
||||
"integrity": "sha512-/+S03c8AGsDYKKBtRDqieTJv2GlkMb0bWjnqOgtF6MkjdUQ9a8ARAtxWf9NgKLGm2+WQr6+/tqJdU8HNGsIDoA==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/path2d-polyfill": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.1.1.tgz",
|
||||
"integrity": "sha512-4Rka5lN+rY/p0CdD8+E+BFv51lFaFvJOrlOhyQ+zjzyQrzyh3ozmxd1vVGGDdIbUFSBtIZLSnspxTgPT0iJhvA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz",
|
||||
"integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"path2d": "0.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist": {
|
||||
|
@ -9045,9 +9033,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/reagraph": {
|
||||
"version": "4.15.26",
|
||||
"resolved": "https://registry.npmjs.org/reagraph/-/reagraph-4.15.26.tgz",
|
||||
"integrity": "sha512-s8xYL9frjoQA1BmPS9tOshR8My9qDSRAiU79YfYO+grleBvhsbXMh7Evlj1ACYh8OR6fLNCdsuK1Micz6fZ7zQ==",
|
||||
"version": "4.15.27",
|
||||
"resolved": "https://registry.npmjs.org/reagraph/-/reagraph-4.15.27.tgz",
|
||||
"integrity": "sha512-BoYWSFdbxeLkEL4lAM9Y/ey0L+liY1is/3+dxZ4pvhlVj4f9RIWZh1IqILY+7t2mRYts5RInsgz0ZAV4t4tIJw==",
|
||||
"dependencies": {
|
||||
"@react-spring/three": "9.6.1",
|
||||
"@react-three/fiber": "8.13.5",
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
"react-tabs": "^6.0.2",
|
||||
"react-toastify": "^9.1.3",
|
||||
"react-tooltip": "^5.26.3",
|
||||
"reagraph": "^4.15.26"
|
||||
"reagraph": "^4.15.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.7.0",
|
||||
|
|
|
@ -27,9 +27,9 @@ export { BiFontFamily as IconText } from 'react-icons/bi';
|
|||
export { BiFont as IconTextOff } from 'react-icons/bi';
|
||||
export { RiTreeLine as IconTree } from 'react-icons/ri';
|
||||
|
||||
export { LuMinimize as IconGraphClosure } from 'react-icons/lu';
|
||||
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
|
||||
export { BiExpand as IconGraphExpand } from 'react-icons/bi';
|
||||
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
|
||||
export { LuExpand as IconGraphExpand } from 'react-icons/lu';
|
||||
export { BiGitBranch as IconGraphInputs } from 'react-icons/bi';
|
||||
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||
|
||||
import DataTable, { createColumnHelper, RowSelectionState } from '@/components/ui/DataTable';
|
||||
import { useConceptOptions } from '@/context/OptionsContext';
|
||||
|
@ -9,8 +9,8 @@ import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
|
|||
import { describeConstituenta } from '@/utils/labels';
|
||||
|
||||
import ConstituentaBadge from '../info/ConstituentaBadge';
|
||||
import Button from '../ui/Button';
|
||||
import FlexColumn from '../ui/FlexColumn';
|
||||
import SelectGraphToolbar from './SelectGraphToolbar';
|
||||
|
||||
interface ConstituentaMultiPickerProps {
|
||||
id?: string;
|
||||
|
@ -19,7 +19,7 @@ interface ConstituentaMultiPickerProps {
|
|||
rows?: number;
|
||||
|
||||
selected: ConstituentaID[];
|
||||
setSelected: React.Dispatch<ConstituentaID[]>;
|
||||
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<IConstituenta>();
|
||||
|
@ -55,26 +55,6 @@ function ConstituentaMultiPicker({ id, schema, prefixID, rows, selected, setSele
|
|||
}
|
||||
}
|
||||
|
||||
const selectBasis = useCallback(() => {
|
||||
if (!schema || selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
const addition = schema.graph.expandAllInputs(selected).filter(id => !selected.includes(id));
|
||||
if (addition.length > 0) {
|
||||
setSelected([...selected, ...addition]);
|
||||
}
|
||||
}, [schema, selected, setSelected]);
|
||||
|
||||
const selectDependant = useCallback(() => {
|
||||
if (!schema || selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
const addition = schema.graph.expandAllOutputs(selected).filter(id => !selected.includes(id));
|
||||
if (addition.length > 0) {
|
||||
setSelected([...selected, ...addition]);
|
||||
}
|
||||
}, [schema, selected, setSelected]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor('alias', {
|
||||
|
@ -98,20 +78,13 @@ function ConstituentaMultiPicker({ id, schema, prefixID, rows, selected, setSele
|
|||
<span className='w-[24ch] select-none whitespace-nowrap'>
|
||||
Выбраны {selected.length} из {schema?.items.length ?? 0}
|
||||
</span>
|
||||
<div className='flex w-full gap-6 text-sm'>
|
||||
<Button
|
||||
text='Влияющие'
|
||||
title='Добавить все конституенты, от которых зависят выбранные'
|
||||
className='w-[7rem] text-sm'
|
||||
onClick={selectBasis}
|
||||
{schema ? (
|
||||
<SelectGraphToolbar
|
||||
graph={schema.graph} // prettier: split lines
|
||||
setSelected={setSelected}
|
||||
className='w-full ml-8'
|
||||
/>
|
||||
<Button
|
||||
text='Зависимые'
|
||||
title='Добавить все конституенты, которые зависят от выбранных'
|
||||
className='w-[7rem] text-sm'
|
||||
onClick={selectDependant}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<DataTable
|
||||
id={id}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { Graph } from '@/models/Graph';
|
||||
|
||||
import {
|
||||
IconGraphCollapse,
|
||||
IconGraphExpand,
|
||||
IconGraphInputs,
|
||||
IconGraphMaximize,
|
||||
IconGraphOutputs,
|
||||
IconReset
|
||||
} from '../Icons';
|
||||
import { CProps } from '../props';
|
||||
import MiniButton from '../ui/MiniButton';
|
||||
|
||||
interface SelectGraphToolbarProps extends CProps.Styling {
|
||||
graph: Graph;
|
||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||
}
|
||||
|
||||
function SelectGraphToolbar({ className, graph, setSelected, ...restProps }: SelectGraphToolbarProps) {
|
||||
return (
|
||||
<div className={clsx('cc-icons', className)} {...restProps}>
|
||||
<MiniButton
|
||||
titleHtml='Сбросить выделение'
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setSelected([])}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='Выделить все влияющие'
|
||||
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setSelected(prev => [...prev, ...graph.expandAllInputs(prev)])}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='Выделить все зависимые'
|
||||
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setSelected(prev => [...prev, ...graph.expandAllOutputs(prev)])}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='<b>Максимизация</b> - дополнение выделения конституентами, зависимыми только от выделенных'
|
||||
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setSelected(prev => graph.maximizePart(prev))}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='Выделить поставщиков'
|
||||
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setSelected(prev => [...prev, ...graph.expandInputs(prev)])}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='Выделить потребителей'
|
||||
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setSelected(prev => [...prev, ...graph.expandOutputs(prev)])}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectGraphToolbar;
|
|
@ -11,7 +11,7 @@ interface ConstituentsTabProps {
|
|||
loading?: boolean;
|
||||
error?: ErrorData;
|
||||
selected: ConstituentaID[];
|
||||
setSelected: React.Dispatch<ConstituentaID[]>;
|
||||
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
||||
}
|
||||
|
||||
function ConstituentsTab({ schema, error, loading, selected, setSelected }: ConstituentsTabProps) {
|
||||
|
|
|
@ -44,7 +44,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
|||
newSelection.push(cst.id);
|
||||
}
|
||||
});
|
||||
controller.setSelection(newSelection);
|
||||
controller.setSelected(newSelection);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,18 +4,13 @@ import {
|
|||
IconDestroy,
|
||||
IconFilter,
|
||||
IconFitImage,
|
||||
IconGraphClosure,
|
||||
IconGraphExpand,
|
||||
IconGraphInputs,
|
||||
IconGraphMaximize,
|
||||
IconGraphOutputs,
|
||||
IconNewItem,
|
||||
IconReset,
|
||||
IconRotate3D,
|
||||
IconText,
|
||||
IconTextOff
|
||||
} from '@/components/Icons';
|
||||
import BadgeHelp from '@/components/man/BadgeHelp';
|
||||
import SelectGraphToolbar from '@/components/select/SelectGraphToolbar';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
@ -101,43 +96,7 @@ function GraphToolbar({
|
|||
) : null}
|
||||
<BadgeHelp topic={HelpTopic.GRAPH_TERM} className='max-w-[calc(100vw-4rem)]' offset={4} />
|
||||
</div>
|
||||
<div className='cc-icons'>
|
||||
<MiniButton
|
||||
titleHtml='<b>[ESC]</b><br/>Сбросить выделение'
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
onClick={controller.deselectAll}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='<b>Замыкание</b> - дополнение выделения влияющими конституентами'
|
||||
icon={<IconGraphClosure size='1.25rem' className='icon-primary' />}
|
||||
disabled={controller.nothingSelected}
|
||||
onClick={controller.selectAllInputs}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='<b>Максимизация</b> - дополнение выделения конституентами, зависимыми только от выделенных'
|
||||
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
|
||||
disabled={controller.nothingSelected}
|
||||
onClick={controller.selectMax}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='Выделить все зависимые'
|
||||
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
|
||||
disabled={controller.nothingSelected}
|
||||
onClick={controller.selectAllOutputs}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='Выделить поставщиков'
|
||||
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
|
||||
disabled={controller.nothingSelected}
|
||||
onClick={controller.selectInputs}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='Выделить потребителей'
|
||||
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
|
||||
disabled={controller.nothingSelected}
|
||||
onClick={controller.selectOutputs}
|
||||
/>
|
||||
</div>
|
||||
<SelectGraphToolbar graph={controller.schema!.graph} setSelected={controller.setSelected} />
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ interface IRSEditContext {
|
|||
canProduceStructure: boolean;
|
||||
nothingSelected: boolean;
|
||||
|
||||
setSelection: (selected: ConstituentaID[]) => void;
|
||||
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
||||
select: (target: ConstituentaID) => void;
|
||||
selectAllInputs: () => void;
|
||||
selectAllOutputs: () => void;
|
||||
|
@ -486,7 +486,7 @@ export const RSEditState = ({
|
|||
canProduceStructure,
|
||||
nothingSelected,
|
||||
|
||||
setSelection: (selected: ConstituentaID[]) => setSelected(selected),
|
||||
setSelected: setSelected,
|
||||
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
|
||||
deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
|
||||
selectAllInputs: () => setSelected(prev => [...prev, ...(model.schema?.graph.expandAllInputs(prev) ?? [])]),
|
||||
|
|
|
@ -2,7 +2,15 @@
|
|||
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { IconFilter, IconSettings } from '@/components/Icons';
|
||||
import {
|
||||
IconFilter,
|
||||
IconGraphCollapse,
|
||||
IconGraphExpand,
|
||||
IconGraphInputs,
|
||||
IconGraphOutputs,
|
||||
IconSettings,
|
||||
IconText
|
||||
} from '@/components/Icons';
|
||||
import Dropdown from '@/components/ui/Dropdown';
|
||||
import DropdownButton from '@/components/ui/DropdownButton';
|
||||
import SearchBar from '@/components/ui/SearchBar';
|
||||
|
@ -24,6 +32,23 @@ interface ConstituentsSearchProps {
|
|||
setFiltered: React.Dispatch<React.SetStateAction<IConstituenta[]>>;
|
||||
}
|
||||
|
||||
function DependencyIcon(mode: DependencyMode, size: string) {
|
||||
switch (mode) {
|
||||
case DependencyMode.ALL:
|
||||
return <IconSettings size={size} />;
|
||||
case DependencyMode.EXPRESSION:
|
||||
return <IconText size={size} />;
|
||||
case DependencyMode.OUTPUTS:
|
||||
return <IconGraphOutputs size={size} />;
|
||||
case DependencyMode.INPUTS:
|
||||
return <IconGraphInputs size={size} />;
|
||||
case DependencyMode.EXPAND_OUTPUTS:
|
||||
return <IconGraphExpand size={size} />;
|
||||
case DependencyMode.EXPAND_INPUTS:
|
||||
return <IconGraphCollapse size={size} />;
|
||||
}
|
||||
}
|
||||
|
||||
function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }: ConstituentsSearchProps) {
|
||||
const [filterMatch, setFilterMatch] = useLocalStorage(storage.cstFilterMatch, CstMatchMode.ALL);
|
||||
const [filterSource, setFilterSource] = useLocalStorage(storage.cstFilterGraph, DependencyMode.ALL);
|
||||
|
@ -121,7 +146,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
|
|||
title='Настройка фильтрации по графу термов'
|
||||
hideTitle={sourceMenu.isOpen}
|
||||
className='h-full pr-2'
|
||||
icon={<IconSettings size='1.25rem' />}
|
||||
icon={DependencyIcon(filterSource, '1.25rem')}
|
||||
text={labelCstSource(filterSource)}
|
||||
onClick={sourceMenu.toggle}
|
||||
/>
|
||||
|
@ -132,13 +157,14 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
|
|||
const source = value as DependencyMode;
|
||||
return (
|
||||
<DropdownButton
|
||||
className='w-[23rem]'
|
||||
className='w-[18rem]'
|
||||
key={`${prefixes.cst_source_list}${index}`}
|
||||
onClick={() => handleSourceChange(source)}
|
||||
>
|
||||
<p>
|
||||
<div className='inline-flex items-center gap-1'>
|
||||
{DependencyIcon(source, '1.25rem')}
|
||||
<b>{labelCstSource(source)}:</b> {describeCstSource(source)}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownButton>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -243,11 +243,11 @@ export function describeCstSource(mode: DependencyMode): string {
|
|||
// prettier-ignore
|
||||
switch (mode) {
|
||||
case DependencyMode.ALL: return 'все конституенты';
|
||||
case DependencyMode.EXPRESSION: return 'идентификаторы из выражения';
|
||||
case DependencyMode.OUTPUTS: return 'прямые ссылки на текущую';
|
||||
case DependencyMode.INPUTS: return 'прямые ссылки из текущей';
|
||||
case DependencyMode.EXPAND_OUTPUTS: return 'опосредованные ссылки на текущую';
|
||||
case DependencyMode.EXPAND_INPUTS: return 'опосредованные ссылки из текущей';
|
||||
case DependencyMode.EXPRESSION: return 'имена из выражения';
|
||||
case DependencyMode.OUTPUTS: return 'прямые исходящие';
|
||||
case DependencyMode.INPUTS: return 'прямые входящие';
|
||||
case DependencyMode.EXPAND_OUTPUTS: return 'цепочка исходящих';
|
||||
case DependencyMode.EXPAND_INPUTS: return 'цепочка входящих';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user