Improve graph UI

This commit is contained in:
IRBorisov 2024-04-04 20:03:41 +03:00
parent 2f98ae90ff
commit 64ebce3082
11 changed files with 120 additions and 116 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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';

View File

@ -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}

View File

@ -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;

View File

@ -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) {

View File

@ -44,7 +44,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
newSelection.push(cst.id);
}
});
controller.setSelection(newSelection);
controller.setSelected(newSelection);
}
}

View File

@ -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>
);
}

View File

@ -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) ?? [])]),

View File

@ -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>
);
})}

View File

@ -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 'цепочка входящих';
}
}