mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Implement graph operations and improve UI
This commit is contained in:
parent
79be1167be
commit
b33dceebf8
|
@ -59,7 +59,7 @@ function ConstituentaMultiPicker({ id, schema, prefixID, rows, selected, setSele
|
||||||
if (!schema || selected.length === 0) {
|
if (!schema || selected.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const addition = schema.graph.expandInputs(selected).filter(id => !selected.includes(id));
|
const addition = schema.graph.expandAllInputs(selected).filter(id => !selected.includes(id));
|
||||||
if (addition.length > 0) {
|
if (addition.length > 0) {
|
||||||
setSelected([...selected, ...addition]);
|
setSelected([...selected, ...addition]);
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ function ConstituentaMultiPicker({ id, schema, prefixID, rows, selected, setSele
|
||||||
if (!schema || selected.length === 0) {
|
if (!schema || selected.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const addition = schema.graph.expandOutputs(selected).filter(id => !selected.includes(id));
|
const addition = schema.graph.expandAllOutputs(selected).filter(id => !selected.includes(id));
|
||||||
if (addition.length > 0) {
|
if (addition.length > 0) {
|
||||||
setSelected([...selected, ...addition]);
|
setSelected([...selected, ...addition]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ function MiniButton({
|
||||||
type='button'
|
type='button'
|
||||||
tabIndex={tabIndex ?? -1}
|
tabIndex={tabIndex ?? -1}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-full',
|
'rounded-lg',
|
||||||
'clr-btn-clear',
|
'clr-btn-clear',
|
||||||
'cursor-pointer disabled:cursor-not-allowed',
|
'cursor-pointer disabled:cursor-not-allowed',
|
||||||
{
|
{
|
||||||
|
|
|
@ -14,7 +14,7 @@ function TabLabel({ label, title, titleHtml, hideTitle, className, ...otherProps
|
||||||
return (
|
return (
|
||||||
<TabImpl
|
<TabImpl
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'min-w-[6rem] h-full',
|
'min-w-[5.5rem] h-full',
|
||||||
'px-2 py-1 flex justify-center',
|
'px-2 py-1 flex justify-center',
|
||||||
'clr-tab',
|
'clr-tab',
|
||||||
'text-sm whitespace-nowrap font-controls',
|
'text-sm whitespace-nowrap font-controls',
|
||||||
|
|
|
@ -18,7 +18,7 @@ interface DlgDeleteCstProps extends Pick<ModalProps, 'hideWindow'> {
|
||||||
|
|
||||||
function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstProps) {
|
function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstProps) {
|
||||||
const [expandOut, setExpandOut] = useState(false);
|
const [expandOut, setExpandOut] = useState(false);
|
||||||
const expansion: number[] = useMemo(() => schema.graph.expandOutputs(selected), [selected, schema.graph]);
|
const expansion: number[] = useMemo(() => schema.graph.expandAllOutputs(selected), [selected, schema.graph]);
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
hideWindow();
|
hideWindow();
|
||||||
|
|
|
@ -88,6 +88,7 @@ function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVe
|
||||||
value={version}
|
value={version}
|
||||||
onChange={event => setVersion(event.target.value)}
|
onChange={event => setVersion(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<div className='cc-icons'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Сохранить изменения'
|
title='Сохранить изменения'
|
||||||
disabled={!isModified || !isValid || processing}
|
disabled={!isModified || !isValid || processing}
|
||||||
|
@ -101,6 +102,7 @@ function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVe
|
||||||
icon={<BiReset size='1.25rem' className='icon-primary' />}
|
icon={<BiReset size='1.25rem' className='icon-primary' />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<TextArea
|
<TextArea
|
||||||
id='dlg_description'
|
id='dlg_description'
|
||||||
spellCheck
|
spellCheck
|
||||||
|
|
|
@ -95,11 +95,22 @@ describe('Testing Graph sort', () => {
|
||||||
|
|
||||||
describe('Testing Graph queries', () => {
|
describe('Testing Graph queries', () => {
|
||||||
test('expand outputs', () => {
|
test('expand outputs', () => {
|
||||||
const graph = new Graph([[1, 2], [2, 3], [2, 5], [5, 6], [6, 1], [7]]);
|
const graph = new Graph([
|
||||||
|
[1, 2], //
|
||||||
|
[2, 3],
|
||||||
|
[2, 5],
|
||||||
|
[5, 6],
|
||||||
|
[6, 1],
|
||||||
|
[7]
|
||||||
|
]);
|
||||||
expect(graph.expandOutputs([])).toStrictEqual([]);
|
expect(graph.expandOutputs([])).toStrictEqual([]);
|
||||||
|
expect(graph.expandAllOutputs([])).toStrictEqual([]);
|
||||||
expect(graph.expandOutputs([3])).toStrictEqual([]);
|
expect(graph.expandOutputs([3])).toStrictEqual([]);
|
||||||
|
expect(graph.expandAllOutputs([3])).toStrictEqual([]);
|
||||||
expect(graph.expandOutputs([7])).toStrictEqual([]);
|
expect(graph.expandOutputs([7])).toStrictEqual([]);
|
||||||
expect(graph.expandOutputs([2, 5])).toStrictEqual([3, 6, 1]);
|
expect(graph.expandAllOutputs([7])).toStrictEqual([]);
|
||||||
|
expect(graph.expandOutputs([2, 5])).toStrictEqual([3, 6]);
|
||||||
|
expect(graph.expandAllOutputs([2, 5])).toStrictEqual([3, 6, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('expand into unique array', () => {
|
test('expand into unique array', () => {
|
||||||
|
@ -109,13 +120,43 @@ describe('Testing Graph queries', () => {
|
||||||
[2, 5],
|
[2, 5],
|
||||||
[3, 5]
|
[3, 5]
|
||||||
]);
|
]);
|
||||||
expect(graph.expandOutputs([1])).toStrictEqual([2, 3, 5]);
|
expect(graph.expandAllOutputs([1])).toStrictEqual([2, 3, 5]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('expand inputs', () => {
|
test('expand inputs', () => {
|
||||||
const graph = new Graph([[1, 2], [2, 3], [2, 5], [5, 6], [6, 1], [7]]);
|
const graph = new Graph([
|
||||||
|
[1, 2], //
|
||||||
|
[2, 3],
|
||||||
|
[2, 5],
|
||||||
|
[5, 6],
|
||||||
|
[6, 1],
|
||||||
|
[7]
|
||||||
|
]);
|
||||||
expect(graph.expandInputs([])).toStrictEqual([]);
|
expect(graph.expandInputs([])).toStrictEqual([]);
|
||||||
|
expect(graph.expandAllInputs([])).toStrictEqual([]);
|
||||||
expect(graph.expandInputs([7])).toStrictEqual([]);
|
expect(graph.expandInputs([7])).toStrictEqual([]);
|
||||||
expect(graph.expandInputs([6])).toStrictEqual([5, 2, 1]);
|
expect(graph.expandAllInputs([7])).toStrictEqual([]);
|
||||||
|
expect(graph.expandInputs([6])).toStrictEqual([5]);
|
||||||
|
expect(graph.expandAllInputs([6])).toStrictEqual([5, 2, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maximize part', () => {
|
||||||
|
const graph = new Graph([
|
||||||
|
[1, 7], //
|
||||||
|
[1, 3],
|
||||||
|
[2, 3],
|
||||||
|
[2, 4],
|
||||||
|
[3, 5],
|
||||||
|
[3, 6],
|
||||||
|
[3, 4],
|
||||||
|
[7, 5],
|
||||||
|
[8]
|
||||||
|
]);
|
||||||
|
expect(graph.maximizePart([])).toStrictEqual([]);
|
||||||
|
expect(graph.maximizePart([8])).toStrictEqual([8]);
|
||||||
|
expect(graph.maximizePart([5])).toStrictEqual([5]);
|
||||||
|
expect(graph.maximizePart([3])).toStrictEqual([3, 6]);
|
||||||
|
expect(graph.maximizePart([3, 2])).toStrictEqual([3, 2, 6, 4]);
|
||||||
|
expect(graph.maximizePart([3, 1])).toStrictEqual([3, 1, 7, 5, 6]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -144,18 +144,42 @@ export class Graph {
|
||||||
|
|
||||||
expandOutputs(origin: number[]): number[] {
|
expandOutputs(origin: number[]): number[] {
|
||||||
const result: number[] = [];
|
const result: number[] = [];
|
||||||
const marked = new Map<number, boolean>();
|
|
||||||
origin.forEach(id => marked.set(id, true));
|
|
||||||
origin.forEach(id => {
|
origin.forEach(id => {
|
||||||
const node = this.nodes.get(id);
|
const node = this.nodes.get(id);
|
||||||
if (node) {
|
if (node) {
|
||||||
node.outputs.forEach(child => {
|
node.outputs.forEach(child => {
|
||||||
if (!marked.get(child) && !result.find(id => id === child)) {
|
if (!origin.includes(child) && !result.includes(child)) {
|
||||||
result.push(child);
|
result.push(child);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
expandInputs(origin: number[]): number[] {
|
||||||
|
const result: number[] = [];
|
||||||
|
origin.forEach(id => {
|
||||||
|
const node = this.nodes.get(id);
|
||||||
|
if (node) {
|
||||||
|
node.inputs.forEach(child => {
|
||||||
|
if (!origin.includes(child) && !result.includes(child)) {
|
||||||
|
result.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
expandAllOutputs(origin: number[]): number[] {
|
||||||
|
const result: number[] = this.expandOutputs(origin);
|
||||||
|
if (result.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const marked = new Map<number, boolean>();
|
||||||
|
origin.forEach(id => marked.set(id, true));
|
||||||
let position = 0;
|
let position = 0;
|
||||||
while (position < result.length) {
|
while (position < result.length) {
|
||||||
const node = this.nodes.get(result[position]);
|
const node = this.nodes.get(result[position]);
|
||||||
|
@ -172,20 +196,14 @@ export class Graph {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
expandInputs(origin: number[]): number[] {
|
expandAllInputs(origin: number[]): number[] {
|
||||||
const result: number[] = [];
|
const result: number[] = this.expandInputs(origin);
|
||||||
|
if (result.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const marked = new Map<number, boolean>();
|
const marked = new Map<number, boolean>();
|
||||||
origin.forEach(id => marked.set(id, true));
|
origin.forEach(id => marked.set(id, true));
|
||||||
origin.forEach(id => {
|
|
||||||
const node = this.nodes.get(id);
|
|
||||||
if (node) {
|
|
||||||
node.inputs.forEach(child => {
|
|
||||||
if (!marked.get(child) && !result.find(id => id === child)) {
|
|
||||||
result.push(child);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let position = 0;
|
let position = 0;
|
||||||
while (position < result.length) {
|
while (position < result.length) {
|
||||||
const node = this.nodes.get(result[position]);
|
const node = this.nodes.get(result[position]);
|
||||||
|
@ -202,6 +220,20 @@ export class Graph {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maximizePart(origin: number[]): number[] {
|
||||||
|
const outputs: number[] = this.expandAllOutputs(origin);
|
||||||
|
const result = [...origin];
|
||||||
|
this.topologicalOrder()
|
||||||
|
.filter(id => outputs.includes(id))
|
||||||
|
.forEach(id => {
|
||||||
|
const node = this.nodes.get(id);
|
||||||
|
if (node?.inputs.every(parent => result.includes(parent))) {
|
||||||
|
result.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
topologicalOrder(): number[] {
|
topologicalOrder(): number[] {
|
||||||
const result: number[] = [];
|
const result: number[] = [];
|
||||||
const marked = new Map<number, boolean>();
|
const marked = new Map<number, boolean>();
|
||||||
|
|
|
@ -27,10 +27,10 @@ export function applyGraphFilter(target: IRSForm, start: number, mode: Dependenc
|
||||||
return target.graph.nodes.get(start)?.inputs;
|
return target.graph.nodes.get(start)?.inputs;
|
||||||
}
|
}
|
||||||
case DependencyMode.EXPAND_OUTPUTS: {
|
case DependencyMode.EXPAND_OUTPUTS: {
|
||||||
return target.graph.expandOutputs([start]);
|
return target.graph.expandAllOutputs([start]);
|
||||||
}
|
}
|
||||||
case DependencyMode.EXPAND_INPUTS: {
|
case DependencyMode.EXPAND_INPUTS: {
|
||||||
return target.graph.expandInputs([start]);
|
return target.graph.expandAllInputs([start]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
|
@ -31,7 +31,7 @@ function ConstituentaToolbar({
|
||||||
onCreate
|
onCreate
|
||||||
}: ConstituentaToolbarProps) {
|
}: ConstituentaToolbarProps) {
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-1 right-4 sm:right-1/2 sm:translate-x-1/2' className='flex'>
|
<Overlay position='top-1 right-4 sm:right-1/2 sm:translate-x-1/2' className='cc-icons'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||||
icon={<FiSave size='1.25rem' className='icon-primary' />}
|
icon={<FiSave size='1.25rem' className='icon-primary' />}
|
||||||
|
|
|
@ -18,7 +18,7 @@ interface ControlsOverlayProps {
|
||||||
|
|
||||||
function ControlsOverlay({ constituenta, disabled, modified, processing, onRename, onEditTerm }: ControlsOverlayProps) {
|
function ControlsOverlay({ constituenta, disabled, modified, processing, onRename, onEditTerm }: ControlsOverlayProps) {
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-1 left-[4.1rem]' className='flex select-none'>
|
<Overlay position='top-1 left-[4.3rem]' className='flex select-none'>
|
||||||
{!disabled || processing ? (
|
{!disabled || processing ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={
|
title={
|
||||||
|
|
|
@ -161,7 +161,7 @@ function EditorRSExpression({
|
||||||
) : null}
|
) : null}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<Overlay position='top-[-0.5rem] right-0 flex'>
|
<Overlay position='top-[-0.5rem] right-0 cc-icons'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Изменить шрифт'
|
title='Изменить шрифт'
|
||||||
onClick={toggleFont}
|
onClick={toggleFont}
|
||||||
|
@ -169,20 +169,17 @@ function EditorRSExpression({
|
||||||
/>
|
/>
|
||||||
{!disabled || model.processing ? (
|
{!disabled || model.processing ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noHover
|
|
||||||
title='Отображение специальной клавиатуры'
|
title='Отображение специальной клавиатуры'
|
||||||
onClick={() => setShowControls(prev => !prev)}
|
onClick={() => setShowControls(prev => !prev)}
|
||||||
icon={<FaRegKeyboard size='1.25rem' className={showControls ? 'icon-primary' : ''} />}
|
icon={<FaRegKeyboard size='1.25rem' className={showControls ? 'icon-primary' : ''} />}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noHover
|
|
||||||
title='Отображение списка конституент'
|
title='Отображение списка конституент'
|
||||||
onClick={onToggleList}
|
onClick={onToggleList}
|
||||||
icon={<BiListUl size='1.25rem' className={showList ? 'icon-primary' : ''} />}
|
icon={<BiListUl size='1.25rem' className={showList ? 'icon-primary' : ''} />}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noHover
|
|
||||||
title='Дерево разбора выражения'
|
title='Дерево разбора выражения'
|
||||||
onClick={handleShowAST}
|
onClick={handleShowAST}
|
||||||
icon={<RiNodeTree size='1.25rem' className='icon-primary' />}
|
icon={<RiNodeTree size='1.25rem' className='icon-primary' />}
|
||||||
|
|
|
@ -117,18 +117,16 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||||
onChange={event => setAlias(event.target.value)}
|
onChange={event => setAlias(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<Overlay position='top-[-0.25rem] right-[-0.25rem] flex'>
|
<Overlay position='top-[-0.25rem] right-[-0.25rem] cc-icons'>
|
||||||
{controller.isMutable ? (
|
{controller.isMutable ? (
|
||||||
<>
|
<>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noHover
|
|
||||||
title={controller.isContentEditable ? 'Создать версию' : 'Переключитесь на актуальную версию'}
|
title={controller.isContentEditable ? 'Создать версию' : 'Переключитесь на актуальную версию'}
|
||||||
disabled={!controller.isContentEditable}
|
disabled={!controller.isContentEditable}
|
||||||
onClick={controller.createVersion}
|
onClick={controller.createVersion}
|
||||||
icon={<LuGitBranchPlus size='1.25rem' className='icon-green' />}
|
icon={<LuGitBranchPlus size='1.25rem' className='icon-green' />}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noHover
|
|
||||||
title={schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
|
title={schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
|
||||||
disabled={!schema || schema?.versions.length === 0}
|
disabled={!schema || schema?.versions.length === 0}
|
||||||
onClick={controller.editVersions}
|
onClick={controller.editVersions}
|
||||||
|
|
|
@ -26,7 +26,7 @@ function RSFormToolbar({ modified, anonymous, subscribed, claimable, onSubmit, o
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]);
|
const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]);
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'>
|
<Overlay position='top-1 right-1/2 translate-x-1/2' className='cc-icons'>
|
||||||
{controller.isContentEditable ? (
|
{controller.isContentEditable ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||||
|
|
|
@ -19,7 +19,7 @@ function RSListToolbar() {
|
||||||
const insertMenu = useDropdown();
|
const insertMenu = useDropdown();
|
||||||
|
|
||||||
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='items-start cc-icons'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
|
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
|
||||||
icon={<BiUpvote size='1.25rem' className='icon-primary' />}
|
icon={<BiUpvote size='1.25rem' className='icon-primary' />}
|
||||||
|
|
|
@ -18,7 +18,6 @@ import { storage, TIMEOUT_GRAPH_REFRESH } from '@/utils/constants';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
import GraphSelectors from './GraphSelectors';
|
import GraphSelectors from './GraphSelectors';
|
||||||
import GraphSidebar from './GraphSidebar';
|
|
||||||
import GraphToolbar from './GraphToolbar';
|
import GraphToolbar from './GraphToolbar';
|
||||||
import TermGraph from './TermGraph';
|
import TermGraph from './TermGraph';
|
||||||
import useGraphFilter from './useGraphFilter';
|
import useGraphFilter from './useGraphFilter';
|
||||||
|
@ -158,6 +157,10 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleDeleteCst();
|
handleDeleteCst();
|
||||||
}
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
controller.deselectAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const graph = useMemo(
|
const graph = useMemo(
|
||||||
|
@ -254,7 +257,6 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
onEdit={onOpenEdit}
|
onEdit={onOpenEdit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<GraphSidebar />
|
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
|
||||||
{graph}
|
{graph}
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { BiGitBranch, BiGitMerge, BiReset } from 'react-icons/bi';
|
|
||||||
import { LuExpand, LuMaximize, LuMinimize } from 'react-icons/lu';
|
|
||||||
|
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
|
||||||
|
|
||||||
function GraphSidebar() {
|
|
||||||
const controller = useRSEdit();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-1 clr-app'>
|
|
||||||
<MiniButton
|
|
||||||
titleHtml='<b>Сбросить выделение</b>'
|
|
||||||
icon={<BiReset size='1.25rem' className='icon-primary' />}
|
|
||||||
onClick={controller.deselectAll}
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GraphSidebar;
|
|
|
@ -1,7 +1,17 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { BiFilterAlt, BiFont, BiFontFamily, BiPlanet, BiPlusCircle, BiTrash } from 'react-icons/bi';
|
import {
|
||||||
import { LuImage } from 'react-icons/lu';
|
BiFilterAlt,
|
||||||
|
BiFont,
|
||||||
|
BiFontFamily,
|
||||||
|
BiGitBranch,
|
||||||
|
BiGitMerge,
|
||||||
|
BiPlanet,
|
||||||
|
BiPlusCircle,
|
||||||
|
BiReset,
|
||||||
|
BiTrash
|
||||||
|
} from 'react-icons/bi';
|
||||||
|
import { LuExpand, LuImage, LuMaximize, LuMinimize } 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';
|
||||||
|
@ -39,7 +49,11 @@ function GraphToolbar({
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-0 pt-1 right-1/2 translate-x-1/2 clr-app' className='flex'>
|
<Overlay
|
||||||
|
position='top-0 pt-1 right-1/2 translate-x-1/2'
|
||||||
|
className='flex flex-col items-center bg-opacity-10 clr-app'
|
||||||
|
>
|
||||||
|
<div className='cc-icons'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Настройки фильтрации узлов и связей'
|
title='Настройки фильтрации узлов и связей'
|
||||||
icon={<BiFilterAlt size='1.25rem' className='icon-primary' />}
|
icon={<BiFilterAlt size='1.25rem' className='icon-primary' />}
|
||||||
|
@ -84,6 +98,44 @@ function GraphToolbar({
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<BadgeHelp topic={HelpTopic.GRAPH_TERM} className='max-w-[calc(100vw-4rem)]' offset={4} />
|
<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={<BiReset size='1.25rem' className='icon-primary' />}
|
||||||
|
onClick={controller.deselectAll}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
titleHtml='<b>Замыкание</b> - дополнение выделения влияющими конституентами'
|
||||||
|
icon={<LuMinimize size='1.25rem' className='icon-primary' />}
|
||||||
|
disabled={controller.nothingSelected}
|
||||||
|
onClick={controller.selectAllInputs}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
titleHtml='<b>Максимизация</b> - дополнение выделения конституентами, зависимыми только от выделенных'
|
||||||
|
icon={<LuMaximize size='1.25rem' className='icon-primary' />}
|
||||||
|
disabled={controller.nothingSelected}
|
||||||
|
onClick={controller.selectMax}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
titleHtml='Выделить все зависимые'
|
||||||
|
icon={<LuExpand size='1.25rem' className='icon-primary' />}
|
||||||
|
disabled={controller.nothingSelected}
|
||||||
|
onClick={controller.selectAllOutputs}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
titleHtml='Выделить поставщиков'
|
||||||
|
icon={<BiGitBranch size='1.25rem' className='icon-primary' />}
|
||||||
|
disabled={controller.nothingSelected}
|
||||||
|
onClick={controller.selectInputs}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
titleHtml='Выделить потребителей'
|
||||||
|
icon={<BiGitMerge size='1.25rem' className='icon-primary' />}
|
||||||
|
disabled={controller.nothingSelected}
|
||||||
|
onClick={controller.selectOutputs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,11 @@ interface IRSEditContext {
|
||||||
|
|
||||||
setSelection: (selected: ConstituentaID[]) => void;
|
setSelection: (selected: ConstituentaID[]) => void;
|
||||||
select: (target: ConstituentaID) => void;
|
select: (target: ConstituentaID) => void;
|
||||||
|
selectAllInputs: () => void;
|
||||||
|
selectAllOutputs: () => void;
|
||||||
|
selectMax: () => void;
|
||||||
|
selectInputs: () => void;
|
||||||
|
selectOutputs: () => void;
|
||||||
deselect: (target: ConstituentaID) => void;
|
deselect: (target: ConstituentaID) => void;
|
||||||
toggleSelect: (target: ConstituentaID) => void;
|
toggleSelect: (target: ConstituentaID) => void;
|
||||||
deselectAll: () => void;
|
deselectAll: () => void;
|
||||||
|
@ -484,6 +489,11 @@ export const RSEditState = ({
|
||||||
setSelection: (selected: ConstituentaID[]) => setSelected(selected),
|
setSelection: (selected: ConstituentaID[]) => setSelected(selected),
|
||||||
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
|
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
|
||||||
deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
|
deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
|
||||||
|
selectAllInputs: () => setSelected(prev => [...prev, ...(model.schema?.graph.expandAllInputs(prev) ?? [])]),
|
||||||
|
selectAllOutputs: () => setSelected(prev => [...prev, ...(model.schema?.graph.expandAllOutputs(prev) ?? [])]),
|
||||||
|
selectOutputs: () => setSelected(prev => [...prev, ...(model.schema?.graph.expandOutputs(prev) ?? [])]),
|
||||||
|
selectInputs: () => setSelected(prev => [...prev, ...(model.schema?.graph.expandInputs(prev) ?? [])]),
|
||||||
|
selectMax: () => setSelected(prev => model.schema?.graph.maximizePart(prev) ?? []),
|
||||||
toggleSelect: (target: ConstituentaID) =>
|
toggleSelect: (target: ConstituentaID) =>
|
||||||
setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
|
setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
|
||||||
deselectAll: () => setSelected([]),
|
deselectAll: () => setSelected([]),
|
||||||
|
|
|
@ -204,7 +204,7 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
title={'Редактирование'}
|
title={'Редактирование'}
|
||||||
hideTitle={editMenu.isOpen}
|
hideTitle={editMenu.isOpen}
|
||||||
className='h-full'
|
className='h-full px-2'
|
||||||
icon={<FiEdit size='1.25rem' className={controller.isContentEditable ? 'icon-green' : 'icon-red'} />}
|
icon={<FiEdit size='1.25rem' className={controller.isContentEditable ? 'icon-green' : 'icon-red'} />}
|
||||||
onClick={editMenu.toggle}
|
onClick={editMenu.toggle}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -179,7 +179,7 @@ export const graphLightT = {
|
||||||
activeFill: '#1DE9AC',
|
activeFill: '#1DE9AC',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
selectedOpacity: 1,
|
selectedOpacity: 1,
|
||||||
inactiveOpacity: 0.5,
|
inactiveOpacity: 1,
|
||||||
label: {
|
label: {
|
||||||
color: '#2A6475',
|
color: '#2A6475',
|
||||||
stroke: '#fff',
|
stroke: '#fff',
|
||||||
|
@ -199,7 +199,7 @@ export const graphLightT = {
|
||||||
activeFill: '#1DE9AC',
|
activeFill: '#1DE9AC',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
selectedOpacity: 1,
|
selectedOpacity: 1,
|
||||||
inactiveOpacity: 0.1,
|
inactiveOpacity: 1,
|
||||||
label: {
|
label: {
|
||||||
stroke: '#fff',
|
stroke: '#fff',
|
||||||
color: '#2A6475',
|
color: '#2A6475',
|
||||||
|
@ -209,13 +209,6 @@ export const graphLightT = {
|
||||||
arrow: {
|
arrow: {
|
||||||
fill: '#D8E6EA',
|
fill: '#D8E6EA',
|
||||||
activeFill: '#1DE9AC'
|
activeFill: '#1DE9AC'
|
||||||
},
|
|
||||||
cluster: {
|
|
||||||
stroke: '#D8E6EA',
|
|
||||||
label: {
|
|
||||||
stroke: '#fff',
|
|
||||||
color: '#2A6475'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -231,7 +224,7 @@ export const graphDarkT = {
|
||||||
activeFill: '#1DE9AC',
|
activeFill: '#1DE9AC',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
selectedOpacity: 1,
|
selectedOpacity: 1,
|
||||||
inactiveOpacity: 0.5,
|
inactiveOpacity: 1,
|
||||||
label: {
|
label: {
|
||||||
stroke: '#1E2026',
|
stroke: '#1E2026',
|
||||||
color: '#ACBAC7',
|
color: '#ACBAC7',
|
||||||
|
@ -251,7 +244,7 @@ export const graphDarkT = {
|
||||||
activeFill: '#1DE9AC',
|
activeFill: '#1DE9AC',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
selectedOpacity: 1,
|
selectedOpacity: 1,
|
||||||
inactiveOpacity: 0.1,
|
inactiveOpacity: 1,
|
||||||
label: {
|
label: {
|
||||||
stroke: '#1E2026',
|
stroke: '#1E2026',
|
||||||
color: '#ACBAC7',
|
color: '#ACBAC7',
|
||||||
|
@ -261,13 +254,6 @@ export const graphDarkT = {
|
||||||
arrow: {
|
arrow: {
|
||||||
fill: '#474B56',
|
fill: '#474B56',
|
||||||
activeFill: '#1DE9AC'
|
activeFill: '#1DE9AC'
|
||||||
},
|
|
||||||
cluster: {
|
|
||||||
stroke: '#474B56',
|
|
||||||
label: {
|
|
||||||
stroke: '#1E2026',
|
|
||||||
color: '#ACBAC7'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -206,4 +206,8 @@
|
||||||
.cc-column {
|
.cc-column {
|
||||||
@apply flex flex-col gap-3;
|
@apply flex flex-col gap-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cc-icons {
|
||||||
|
@apply flex gap-1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user