Refactor constituenta selection and tooltip visibility

This commit is contained in:
IRBorisov 2024-02-03 15:33:28 +03:00
parent 5b3862d46c
commit 2b7a4c04ce
17 changed files with 268 additions and 305 deletions

View File

@ -1,19 +1,19 @@
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
interface SelectedCounterProps { interface SelectedCounterProps {
total: number; totalCount: number;
selected: number; selectedCount: number;
position?: string; position?: string;
hideZero?: boolean; hideZero?: boolean;
} }
function SelectedCounter({ total, selected, hideZero, position = 'top-0 left-0' }: SelectedCounterProps) { function SelectedCounter({ totalCount, selectedCount, hideZero, position = 'top-0 left-0' }: SelectedCounterProps) {
if (selected === 0 && hideZero) { if (selectedCount === 0 && hideZero) {
return null; return null;
} }
return ( return (
<Overlay position={`px-2 ${position}`} className='select-none whitespace-nowrap clr-app'> <Overlay position={`px-2 ${position}`} className='select-none whitespace-nowrap clr-app'>
Выбор {selected} из {total} Выбор {selectedCount} из {totalCount}
</Overlay> </Overlay>
); );
} }

View File

@ -9,6 +9,7 @@ interface ButtonProps extends CProps.Control, CProps.Colors, CProps.Button {
icon?: React.ReactNode; icon?: React.ReactNode;
dense?: boolean; dense?: boolean;
hideTitle?: boolean;
loading?: boolean; loading?: boolean;
} }
@ -19,6 +20,7 @@ function Button({
loading, loading,
dense, dense,
disabled, disabled,
hideTitle,
noBorder, noBorder,
noOutline, noOutline,
colors = 'clr-btn-default', colors = 'clr-btn-default',
@ -46,6 +48,7 @@ function Button({
)} )}
data-tooltip-id={title ? globalIDs.tooltip : undefined} data-tooltip-id={title ? globalIDs.tooltip : undefined}
data-tooltip-content={title} data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
{...restProps} {...restProps}
> >
{icon ? icon : null} {icon ? icon : null}

View File

@ -7,9 +7,10 @@ import { CProps } from '../props';
interface MiniButtonProps extends CProps.Button { interface MiniButtonProps extends CProps.Button {
icon: React.ReactNode; icon: React.ReactNode;
noHover?: boolean; noHover?: boolean;
hideTitle?: boolean;
} }
function MiniButton({ icon, noHover, tabIndex, title, className, ...restProps }: MiniButtonProps) { function MiniButton({ icon, noHover, hideTitle, tabIndex, title, className, ...restProps }: MiniButtonProps) {
return ( return (
<button <button
type='button' type='button'
@ -27,6 +28,7 @@ function MiniButton({ icon, noHover, tabIndex, title, className, ...restProps }:
)} )}
data-tooltip-id={title ? globalIDs.tooltip : undefined} data-tooltip-id={title ? globalIDs.tooltip : undefined}
data-tooltip-content={title} data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
{...restProps} {...restProps}
> >
{icon} {icon}

View File

@ -10,6 +10,7 @@ interface SelectorButtonProps extends CProps.Button {
colors?: string; colors?: string;
transparent?: boolean; transparent?: boolean;
hideTitle?: boolean;
} }
function SelectorButton({ function SelectorButton({
@ -19,13 +20,12 @@ function SelectorButton({
colors = 'clr-btn-default', colors = 'clr-btn-default',
className, className,
transparent, transparent,
hideTitle,
...restProps ...restProps
}: SelectorButtonProps) { }: SelectorButtonProps) {
return ( return (
<button <button
type='button' type='button'
data-tooltip-id={title ? globalIDs.tooltip : undefined}
data-tooltip-content={title}
className={clsx( className={clsx(
'px-1 flex flex-start items-center gap-1', 'px-1 flex flex-start items-center gap-1',
'text-sm font-controls select-none', 'text-sm font-controls select-none',
@ -38,6 +38,9 @@ function SelectorButton({
className, className,
!transparent && colors !transparent && colors
)} )}
data-tooltip-id={title ? globalIDs.tooltip : undefined}
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
{...restProps} {...restProps}
> >
{icon ? icon : null} {icon ? icon : null}

View File

@ -47,6 +47,7 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
transparent transparent
tabIndex={-1} tabIndex={-1}
title='Список фильтров' title='Список фильтров'
hideTitle={strategyMenu.isOpen}
className='h-full' className='h-full'
icon={<BiFilterAlt size='1.25rem' />} icon={<BiFilterAlt size='1.25rem' />}
text={labelLibraryFilter(value)} text={labelLibraryFilter(value)}

View File

@ -1,12 +1,11 @@
'use client'; 'use client';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import { Dispatch, SetStateAction, useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useRSForm } from '@/context/RSFormContext';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { CstType, IConstituenta, ICstCreateData, ICstRenameData } from '@/models/rsform'; import { CstType, IConstituenta, IRSForm } from '@/models/rsform';
import { globalIDs } from '@/utils/constants'; import { globalIDs } from '@/utils/constants';
import ViewConstituents from '../ViewConstituents'; import ViewConstituents from '../ViewConstituents';
@ -20,79 +19,43 @@ const UNFOLDED_HEIGHT = '59.1rem';
const SIDELIST_HIDE_THRESHOLD = 1100; // px const SIDELIST_HIDE_THRESHOLD = 1100; // px
interface EditorConstituentaProps { interface EditorConstituentaProps {
schema?: IRSForm;
isMutable: boolean; isMutable: boolean;
activeID?: number; activeCst?: IConstituenta;
activeCst?: IConstituenta | undefined;
isModified: boolean; isModified: boolean;
setIsModified: Dispatch<SetStateAction<boolean>>; setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
onOpenEdit: (cstID: number) => void; onOpenEdit: (cstID: number) => void;
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void; onClone: () => void;
onRenameCst: (initial: ICstRenameData) => void; onCreate: (type?: CstType) => void;
onRename: () => void;
onEditTerm: () => void; onEditTerm: () => void;
onDeleteCst: (selected: number[], callback?: (items: number[]) => void) => void; onDelete: () => void;
} }
function EditorConstituenta({ function EditorConstituenta({
schema,
isMutable, isMutable,
isModified, isModified,
setIsModified, setIsModified,
activeID,
activeCst, activeCst,
onEditTerm, onEditTerm,
onCreateCst, onClone,
onRenameCst, onCreate,
onRename,
onOpenEdit, onOpenEdit,
onDeleteCst onDelete
}: EditorConstituentaProps) { }: EditorConstituentaProps) {
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const { schema } = useRSForm();
const [showList, setShowList] = useLocalStorage('rseditor-show-list', true); const [showList, setShowList] = useLocalStorage('rseditor-show-list', true);
const [toggleReset, setToggleReset] = useState(false); const [toggleReset, setToggleReset] = useState(false);
const disabled = useMemo(() => !activeCst || !isMutable, [activeCst, isMutable]); const disabled = useMemo(() => !activeCst || !isMutable, [activeCst, isMutable]);
function handleDelete() {
if (!schema || !activeID) {
return;
}
onDeleteCst([activeID]);
}
function handleCreate() { function handleCreate() {
if (!activeID || !schema) { onCreate(activeCst?.cst_type);
return;
}
const data: ICstCreateData = {
insert_after: activeID,
cst_type: activeCst?.cst_type ?? CstType.BASE,
alias: '',
term_raw: '',
definition_formal: '',
definition_raw: '',
convention: '',
term_forms: []
};
onCreateCst(data);
}
function handleClone() {
if (!activeID || !schema || !activeCst) {
return;
}
const data: ICstCreateData = {
insert_after: activeID,
cst_type: activeCst.cst_type,
alias: '',
term_raw: activeCst.term_raw,
definition_formal: activeCst.definition_formal,
definition_raw: activeCst.definition_raw,
convention: activeCst.convention,
term_forms: activeCst.term_forms
};
onCreateCst(data, true);
} }
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) { function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
@ -125,7 +88,7 @@ function EditorConstituenta({
function processAltKey(code: string): boolean { function processAltKey(code: string): boolean {
switch (code) { switch (code) {
case 'KeyV': case 'KeyV':
handleClone(); onClone();
return true; return true;
} }
return false; return false;
@ -138,8 +101,8 @@ function EditorConstituenta({
isModified={isModified} isModified={isModified}
onSubmit={initiateSubmit} onSubmit={initiateSubmit}
onReset={() => setToggleReset(prev => !prev)} onReset={() => setToggleReset(prev => !prev)}
onDelete={handleDelete} onDelete={onDelete}
onClone={handleClone} onClone={onClone}
onCreate={handleCreate} onCreate={handleCreate}
/> />
<div tabIndex={-1} className='flex max-w-[95rem]' onKeyDown={handleInput}> <div tabIndex={-1} className='flex max-w-[95rem]' onKeyDown={handleInput}>
@ -153,7 +116,7 @@ function EditorConstituenta({
onToggleList={() => setShowList(prev => !prev)} onToggleList={() => setShowList(prev => !prev)}
setIsModified={setIsModified} setIsModified={setIsModified}
onEditTerm={onEditTerm} onEditTerm={onEditTerm}
onRenameCst={onRenameCst} onRename={onRename}
/> />
<AnimatePresence> <AnimatePresence>
{showList && windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD ? ( {showList && windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD ? (
@ -161,7 +124,7 @@ function EditorConstituenta({
schema={schema} schema={schema}
expression={activeCst?.definition_formal ?? ''} expression={activeCst?.definition_formal ?? ''}
baseHeight={UNFOLDED_HEIGHT} baseHeight={UNFOLDED_HEIGHT}
activeID={activeID} activeID={activeCst?.id}
onOpenEdit={onOpenEdit} onOpenEdit={onOpenEdit}
/> />
) : null} ) : null}

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { Dispatch, SetStateAction, useEffect, useLayoutEffect, useState } from 'react'; import { useEffect, useLayoutEffect, useState } from 'react';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
import { LiaEdit } from 'react-icons/lia'; import { LiaEdit } from 'react-icons/lia';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -12,7 +12,7 @@ import Overlay from '@/components/ui/Overlay';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import { IConstituenta, ICstRenameData, ICstUpdateData } from '@/models/rsform'; import { IConstituenta, ICstUpdateData } from '@/models/rsform';
import { classnames } from '@/utils/constants'; import { classnames } from '@/utils/constants';
import { labelCstTypification } from '@/utils/labels'; import { labelCstTypification } from '@/utils/labels';
@ -27,10 +27,10 @@ interface FormConstituentaProps {
isModified: boolean; isModified: boolean;
toggleReset: boolean; toggleReset: boolean;
setIsModified: Dispatch<SetStateAction<boolean>>; setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
onToggleList: () => void; onToggleList: () => void;
onRenameCst: (initial: ICstRenameData) => void; onRename: () => void;
onEditTerm: () => void; onEditTerm: () => void;
} }
@ -42,7 +42,7 @@ function FormConstituenta({
setIsModified, setIsModified,
constituenta, constituenta,
toggleReset, toggleReset,
onRenameCst, onRename,
onEditTerm, onEditTerm,
onToggleList onToggleList
}: FormConstituentaProps) { }: FormConstituentaProps) {
@ -109,18 +109,6 @@ function FormConstituenta({
cstUpdate(data, () => toast.success('Изменения сохранены')); cstUpdate(data, () => toast.success('Изменения сохранены'));
} }
function handleRename() {
if (!constituenta) {
return;
}
const data: ICstRenameData = {
id: constituenta.id,
alias: constituenta.alias,
cst_type: constituenta.cst_type
};
onRenameCst(data);
}
return ( return (
<> <>
<Overlay position='top-1 left-[4.1rem]' className='flex select-none'> <Overlay position='top-1 left-[4.1rem]' className='flex select-none'>
@ -139,7 +127,7 @@ function FormConstituenta({
noHover noHover
title='Переименовать конституенту' title='Переименовать конституенту'
disabled={disabled} disabled={disabled}
onClick={handleRename} onClick={onRename}
icon={<LiaEdit size='1rem' className={!disabled ? 'clr-text-primary' : ''} />} icon={<LiaEdit size='1rem' className={!disabled ? 'clr-text-primary' : ''} />}
/> />
</Overlay> </Overlay>

View File

@ -1,7 +1,5 @@
'use client'; 'use client';
import { Dispatch, SetStateAction } from 'react';
import InfoLibraryItem from '@/components/InfoLibraryItem'; import InfoLibraryItem from '@/components/InfoLibraryItem';
import Divider from '@/components/ui/Divider'; import Divider from '@/components/ui/Divider';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
@ -17,7 +15,7 @@ interface EditorRSFormProps {
isModified: boolean; isModified: boolean;
isMutable: boolean; isMutable: boolean;
setIsModified: Dispatch<SetStateAction<boolean>>; setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
onDestroy: () => void; onDestroy: () => void;
onClaim: () => void; onClaim: () => void;
onShare: () => void; onShare: () => void;

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { Dispatch, SetStateAction, useEffect, useLayoutEffect, useState } from 'react'; import { useEffect, useLayoutEffect, useState } from 'react';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -19,7 +19,7 @@ interface FormRSFormProps {
id?: string; id?: string;
disabled: boolean; disabled: boolean;
isModified: boolean; isModified: boolean;
setIsModified: Dispatch<SetStateAction<boolean>>; setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
} }
function FormRSForm({ id, disabled, isModified, setIsModified }: FormRSFormProps) { function FormRSForm({ id, disabled, isModified, setIsModified }: FormRSFormProps) {

View File

@ -5,46 +5,59 @@ import { useLayoutEffect, useState } from 'react';
import { type RowSelectionState } from '@/components/DataTable'; import { type RowSelectionState } from '@/components/DataTable';
import SelectedCounter from '@/components/SelectedCounter'; import SelectedCounter from '@/components/SelectedCounter';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import { CstType, ICstCreateData, ICstMovetoData } from '@/models/rsform'; import { CstType, ICstMovetoData } from '@/models/rsform';
import RSListToolbar from './RSListToolbar'; import RSListToolbar from './RSListToolbar';
import RSTable from './RSTable'; import RSTable from './RSTable';
interface EditorRSListProps { interface EditorRSListProps {
isMutable: boolean; isMutable: boolean;
selected: number[];
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
onOpenEdit: (cstID: number) => void; onOpenEdit: (cstID: number) => void;
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void; onClone: () => void;
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void; onCreate: (type?: CstType) => void;
onDelete: () => void;
} }
function EditorRSList({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: EditorRSListProps) { function EditorRSList({
selected,
setSelected,
isMutable,
onOpenEdit,
onClone,
onCreate,
onDelete
}: EditorRSListProps) {
const { schema, cstMoveTo } = useRSForm(); const { schema, cstMoveTo } = useRSForm();
const [selected, setSelected] = useState<number[]>([]);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
useLayoutEffect(() => { useLayoutEffect(() => {
if (!schema || Object.keys(rowSelection).length === 0) { if (!schema || selected.length === 0) {
setRowSelection({});
} else {
const newRowSelection: RowSelectionState = {};
schema.items.forEach((cst, index) => {
newRowSelection[String(index)] = selected.includes(cst.id);
});
setRowSelection(newRowSelection);
}
}, [selected, schema]);
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
if (!schema) {
setSelected([]); setSelected([]);
} else { } else {
const selected: number[] = []; const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
schema.items.forEach((cst, index) => { const newSelection: number[] = [];
if (rowSelection[String(index)] === true) { schema?.items.forEach((cst, index) => {
selected.push(cst.id); if (newRowSelection[String(index)] === true) {
newSelection.push(cst.id);
} }
}); });
setSelected(selected); setSelected(newSelection);
} }
}, [rowSelection, schema]);
// Delete selected constituents
function handleDelete() {
if (!schema) {
return;
}
onDeleteCst(selected, () => {
setRowSelection({});
});
} }
// Move selected cst up // Move selected cst up
@ -65,13 +78,7 @@ function EditorRSList({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Edito
items: selected, items: selected,
move_to: target move_to: target
}; };
cstMoveTo(data, () => { cstMoveTo(data);
const newSelection: RowSelectionState = {};
selected.forEach((_, index) => {
newSelection[String(target + index - 1)] = true;
});
setRowSelection(newSelection);
});
} }
// Move selected cst down // Move selected cst down
@ -96,57 +103,7 @@ function EditorRSList({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Edito
items: selected, items: selected,
move_to: target move_to: target
}; };
cstMoveTo(data, () => { cstMoveTo(data);
const newSelection: RowSelectionState = {};
selected.forEach((_, index) => {
newSelection[String(target + index - 1)] = true;
});
setRowSelection(newSelection);
});
}
function handleCreateCst(type?: CstType) {
if (!schema) {
return;
}
const selectedPosition = selected.reduce((prev, cstID) => {
const position = schema.items.findIndex(cst => cst.id === cstID);
return Math.max(position, prev);
}, -1);
const insert_where = selectedPosition >= 0 ? schema.items[selectedPosition].id : undefined;
const data: ICstCreateData = {
insert_after: insert_where ?? null,
cst_type: type ?? CstType.BASE,
alias: '',
term_raw: '',
definition_formal: '',
definition_raw: '',
convention: '',
term_forms: []
};
onCreateCst(data, type !== undefined);
}
// Clone selected
function handleClone() {
if (selected.length < 1 || !schema) {
return;
}
const activeCst = schema.items.find(cst => cst.id === selected[0]);
if (!activeCst) {
return;
}
const data: ICstCreateData = {
insert_after: activeCst.id,
cst_type: activeCst.cst_type,
alias: '',
term_raw: activeCst.term_raw,
definition_formal: activeCst.definition_formal,
definition_raw: activeCst.definition_raw,
convention: activeCst.convention,
term_forms: activeCst.term_forms
};
onCreateCst(data, true);
} }
// Implement hotkeys for working with constituents table // Implement hotkeys for working with constituents table
@ -156,7 +113,7 @@ function EditorRSList({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Edito
} }
if (event.key === 'Delete' && selected.length > 0) { if (event.key === 'Delete' && selected.length > 0) {
event.preventDefault(); event.preventDefault();
handleDelete(); onDelete();
return; return;
} }
if (!event.altKey || event.shiftKey) { if (!event.altKey || event.shiftKey) {
@ -174,21 +131,21 @@ function EditorRSList({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Edito
switch (code) { switch (code) {
case 'ArrowUp': handleMoveUp(); return true; case 'ArrowUp': handleMoveUp(); return true;
case 'ArrowDown': handleMoveDown(); return true; case 'ArrowDown': handleMoveDown(); return true;
case 'KeyV': handleClone(); return true; case 'KeyV': onClone(); return true;
} }
} }
// prettier-ignore // prettier-ignore
switch (code) { switch (code) {
case 'Backquote': handleCreateCst(); return true; case 'Backquote': onCreate(); return true;
case 'Digit1': handleCreateCst(CstType.BASE); return true; case 'Digit1': onCreate(CstType.BASE); return true;
case 'Digit2': handleCreateCst(CstType.STRUCTURED); return true; case 'Digit2': onCreate(CstType.STRUCTURED); return true;
case 'Digit3': handleCreateCst(CstType.TERM); return true; case 'Digit3': onCreate(CstType.TERM); return true;
case 'Digit4': handleCreateCst(CstType.AXIOM); return true; case 'Digit4': onCreate(CstType.AXIOM); return true;
case 'KeyQ': handleCreateCst(CstType.FUNCTION); return true; case 'KeyQ': onCreate(CstType.FUNCTION); return true;
case 'KeyW': handleCreateCst(CstType.PREDICATE); return true; case 'KeyW': onCreate(CstType.PREDICATE); return true;
case 'Digit5': handleCreateCst(CstType.CONSTANT); return true; case 'Digit5': onCreate(CstType.CONSTANT); return true;
case 'Digit6': handleCreateCst(CstType.THEOREM); return true; case 'Digit6': onCreate(CstType.THEOREM); return true;
} }
return false; return false;
} }
@ -196,8 +153,8 @@ function EditorRSList({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Edito
return ( return (
<div tabIndex={-1} className='outline-none' onKeyDown={handleTableKey}> <div tabIndex={-1} className='outline-none' onKeyDown={handleTableKey}>
<SelectedCounter <SelectedCounter
total={schema?.stats?.count_all ?? 0} totalCount={schema?.stats?.count_all ?? 0}
selected={selected.length} selectedCount={selected.length}
position='top-[0.3rem] left-2' position='top-[0.3rem] left-2'
/> />
@ -206,9 +163,9 @@ function EditorRSList({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Edito
isMutable={isMutable} isMutable={isMutable}
onMoveUp={handleMoveUp} onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown} onMoveDown={handleMoveDown}
onClone={handleClone} onClone={onClone}
onCreate={handleCreateCst} onCreate={onCreate}
onDelete={handleDelete} onDelete={onDelete}
/> />
<div className='pt-[2.3rem] border-b' /> <div className='pt-[2.3rem] border-b' />
@ -216,9 +173,9 @@ function EditorRSList({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Edito
<RSTable <RSTable
items={schema?.items} items={schema?.items}
selected={rowSelection} selected={rowSelection}
setSelected={setRowSelection} setSelected={handleRowSelection}
onEdit={onOpenEdit} onEdit={onOpenEdit}
onCreateNew={() => handleCreateCst()} onCreateNew={onCreate}
/> />
</div> </div>
); );

View File

@ -67,6 +67,7 @@ function RSListToolbar({
<div ref={insertMenu.ref}> <div ref={insertMenu.ref}>
<MiniButton <MiniButton
title='Добавить пустую конституенту' title='Добавить пустую конституенту'
hideTitle={insertMenu.isOpen}
icon={<BiDownArrowCircle size='1.25rem' className={isMutable ? 'clr-text-success' : ''} />} icon={<BiDownArrowCircle size='1.25rem' className={isMutable ? 'clr-text-success' : ''} />}
disabled={!isMutable} disabled={!isMutable}
onClick={insertMenu.toggle} onClick={insertMenu.toggle}

View File

@ -8,12 +8,11 @@ import { GraphEdge, GraphNode, LayoutTypes } from 'reagraph';
import InfoConstituenta from '@/components/InfoConstituenta'; import InfoConstituenta from '@/components/InfoConstituenta';
import SelectedCounter from '@/components/SelectedCounter'; import SelectedCounter from '@/components/SelectedCounter';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import { useRSForm } from '@/context/RSFormContext';
import { useConceptTheme } from '@/context/ThemeContext'; import { useConceptTheme } from '@/context/ThemeContext';
import DlgGraphParams from '@/dialogs/DlgGraphParams'; import DlgGraphParams from '@/dialogs/DlgGraphParams';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import { GraphColoringScheme, GraphFilterParams } from '@/models/miscellaneous'; import { GraphColoringScheme, GraphFilterParams } from '@/models/miscellaneous';
import { CstType, ICstCreateData } from '@/models/rsform'; import { CstType, IRSForm } from '@/models/rsform';
import { colorBgGraphNode } from '@/styling/color'; import { colorBgGraphNode } from '@/styling/color';
import { classnames, TIMEOUT_GRAPH_REFRESH } from '@/utils/constants'; import { classnames, TIMEOUT_GRAPH_REFRESH } from '@/utils/constants';
@ -25,16 +24,25 @@ import ViewHidden from './ViewHidden';
interface EditorTermGraphProps { interface EditorTermGraphProps {
isMutable: boolean; isMutable: boolean;
selected: number[];
schema?: IRSForm;
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
onOpenEdit: (cstID: number) => void; onOpenEdit: (cstID: number) => void;
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void; onCreate: (type: CstType, definition: string) => void;
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void; onDelete: () => void;
} }
function EditorTermGraph({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) { function EditorTermGraph({
const { schema } = useRSForm(); schema,
selected,
setSelected,
isMutable,
onOpenEdit,
onCreate,
onDelete
}: EditorTermGraphProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
const [toggleDataUpdate, setToggleDataUpdate] = useState(false);
const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>('graph_filter', { const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>('graph_filter', {
noHermits: true, noHermits: true,
noTemplates: false, noTemplates: false,
@ -51,14 +59,10 @@ function EditorTermGraph({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Ed
allowTheorem: true allowTheorem: true
}); });
const [showParamsDialog, setShowParamsDialog] = useState(false); const [showParamsDialog, setShowParamsDialog] = useState(false);
const filtered = useGraphFilter(schema, filterParams, toggleDataUpdate); const filtered = useGraphFilter(schema, filterParams);
const [selectedGraph, setSelectedGraph] = useState<number[]>([]);
const [hidden, setHidden] = useState<number[]>([]); const [hidden, setHidden] = useState<number[]>([]);
const [selectedHidden, setSelectedHidden] = useState<number[]>([]);
const selected: number[] = useMemo(() => {
return [...selectedHidden, ...selectedGraph];
}, [selectedHidden, selectedGraph]);
const nothingSelected = useMemo(() => selected.length === 0, [selected]); const nothingSelected = useMemo(() => selected.length === 0, [selected]);
const [layout, setLayout] = useLocalStorage<LayoutTypes>('graph_layout', 'treeTd2d'); const [layout, setLayout] = useLocalStorage<LayoutTypes>('graph_layout', 'treeTd2d');
@ -72,7 +76,6 @@ function EditorTermGraph({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Ed
}, [schema?.items, hoverID]); }, [schema?.items, hoverID]);
const [toggleResetView, setToggleResetView] = useState(false); const [toggleResetView, setToggleResetView] = useState(false);
const [toggleResetSelection, setToggleResetSelection] = useState(false);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!schema) { if (!schema) {
@ -85,9 +88,8 @@ function EditorTermGraph({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Ed
} }
}); });
setHidden(newDismissed); setHidden(newDismissed);
setSelectedHidden([]);
setHoverID(undefined); setHoverID(undefined);
}, [schema, filtered, toggleDataUpdate]); }, [schema, filtered]);
const nodes: GraphNode[] = useMemo(() => { const nodes: GraphNode[] = useMemo(() => {
const result: GraphNode[] = []; const result: GraphNode[] = [];
@ -123,15 +125,20 @@ function EditorTermGraph({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Ed
return result; return result;
}, [filtered.nodes]); }, [filtered.nodes]);
const handleGraphSelection = useCallback(
(newID: number) => {
setSelected(prev => [...prev, newID]);
},
[setSelected]
);
function toggleDismissed(cstID: number) { function toggleDismissed(cstID: number) {
setSelectedHidden(prev => { setSelected(prev => {
const index = prev.findIndex(id => cstID === id); if (prev.includes(cstID)) {
if (index !== -1) { return [...prev.filter(id => id !== cstID)];
prev.splice(index, 1);
} else { } else {
prev.push(cstID); return [...prev, cstID];
} }
return [...prev];
}); });
} }
@ -139,29 +146,15 @@ function EditorTermGraph({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Ed
if (!schema) { if (!schema) {
return; return;
} }
const data: ICstCreateData = { const definition = selected.map(id => schema.items.find(cst => cst.id === id)!.alias).join(' ');
insert_after: null, onCreate(selected.length === 0 ? CstType.BASE : CstType.TERM, definition);
cst_type: selected.length === 0 ? CstType.BASE : CstType.TERM,
alias: '',
term_raw: '',
definition_formal: selected.map(id => schema.items.find(cst => cst.id === id)!.alias).join(' '),
definition_raw: '',
convention: '',
term_forms: []
};
onCreateCst(data);
} }
function handleDeleteCst() { function handleDeleteCst() {
if (!schema || selected.length === 0) { if (!schema || selected.length === 0) {
return; return;
} }
onDeleteCst(selected, () => { onDelete();
setHidden([]);
setSelectedHidden([]);
setToggleResetSelection(prev => !prev);
setToggleDataUpdate(prev => !prev);
});
} }
function handleChangeLayout(newLayout: LayoutTypes) { function handleChangeLayout(newLayout: LayoutTypes) {
@ -206,8 +199,8 @@ function EditorTermGraph({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Ed
<SelectedCounter <SelectedCounter
hideZero hideZero
total={schema?.stats?.count_all ?? 0} totalCount={schema?.stats?.count_all ?? 0}
selected={selected.length} selectedCount={selected.length}
position='top-[0.3rem] left-0' position='top-[0.3rem] left-0'
/> />
@ -249,7 +242,7 @@ function EditorTermGraph({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Ed
/> />
<ViewHidden <ViewHidden
items={hidden} items={hidden}
selected={selectedHidden} selected={selected}
schema={schema!} schema={schema!}
coloringScheme={coloringScheme} coloringScheme={coloringScheme}
toggleSelection={toggleDismissed} toggleSelection={toggleDismissed}
@ -260,15 +253,15 @@ function EditorTermGraph({ isMutable, onOpenEdit, onCreateCst, onDeleteCst }: Ed
<TermGraph <TermGraph
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
selectedIDs={selected}
layout={layout} layout={layout}
is3D={is3D} is3D={is3D}
orbit={orbit} orbit={orbit}
setSelected={setSelectedGraph} onSelect={handleGraphSelection}
setHoverID={setHoverID} setHoverID={setHoverID}
onEdit={onOpenEdit} onEdit={onOpenEdit}
onDeselect={() => setSelectedHidden([])} onDeselectAll={() => setSelected([])}
toggleResetView={toggleResetView} toggleResetView={toggleResetView}
toggleResetSelection={toggleResetSelection}
/> />
</div> </div>
); );

View File

@ -10,18 +10,18 @@ import { resources } from '@/utils/constants';
interface TermGraphProps { interface TermGraphProps {
nodes: GraphNode[]; nodes: GraphNode[];
edges: GraphEdge[]; edges: GraphEdge[];
selectedIDs: number[];
layout: LayoutTypes; layout: LayoutTypes;
is3D: boolean; is3D: boolean;
orbit: boolean; orbit: boolean;
setSelected: (selected: number[]) => void;
setHoverID: (newID: number | undefined) => void; setHoverID: (newID: number | undefined) => void;
onEdit: (cstID: number) => void; onEdit: (cstID: number) => void;
onDeselect: () => void; onSelect: (newID: number) => void;
onDeselectAll: () => void;
toggleResetView: boolean; toggleResetView: boolean;
toggleResetSelection: boolean;
} }
const TREE_SIZE_MILESTONE = 50; const TREE_SIZE_MILESTONE = 50;
@ -29,29 +29,28 @@ const TREE_SIZE_MILESTONE = 50;
function TermGraph({ function TermGraph({
nodes, nodes,
edges, edges,
selectedIDs,
layout, layout,
is3D, is3D,
orbit, orbit,
toggleResetView, toggleResetView,
toggleResetSelection,
setHoverID, setHoverID,
onEdit, onEdit,
setSelected, onSelect,
onDeselect onDeselectAll
}: TermGraphProps) { }: TermGraphProps) {
const { noNavigation, darkMode } = useConceptTheme(); const { noNavigation, darkMode } = useConceptTheme();
const graphRef = useRef<GraphCanvasRef | null>(null); const graphRef = useRef<GraphCanvasRef | null>(null);
const { selections, actives, onNodeClick, clearSelections, onCanvasClick, onNodePointerOver, onNodePointerOut } = const { selections, actives, setSelections, onCanvasClick, onNodePointerOver, onNodePointerOut } = useSelection({
useSelection({ ref: graphRef,
ref: graphRef, nodes,
nodes, edges,
edges, type: 'multi', // 'single' | 'multi' | 'multiModifier'
type: 'multi', // 'single' | 'multi' | 'multiModifier' pathSelectionType: 'out',
pathSelectionType: 'out', pathHoverType: 'all',
pathHoverType: 'all', focusOnSelect: false
focusOnSelect: false });
});
const handleHoverIn = useCallback( const handleHoverIn = useCallback(
(node: GraphNode) => { (node: GraphNode) => {
@ -73,19 +72,19 @@ function TermGraph({
(node: GraphNode) => { (node: GraphNode) => {
if (selections.includes(node.id)) { if (selections.includes(node.id)) {
onEdit(Number(node.id)); onEdit(Number(node.id));
return; } else {
onSelect(Number(node.id));
} }
if (onNodeClick) onNodeClick(node);
}, },
[onNodeClick, selections, onEdit] [onSelect, selections, onEdit]
); );
const handleCanvasClick = useCallback( const handleCanvasClick = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
onDeselect(); onDeselectAll();
if (onCanvasClick) onCanvasClick(event); if (onCanvasClick) onCanvasClick(event);
}, },
[onCanvasClick, onDeselect] [onCanvasClick, onDeselectAll]
); );
useLayoutEffect(() => { useLayoutEffect(() => {
@ -94,12 +93,9 @@ function TermGraph({
}, [toggleResetView]); }, [toggleResetView]);
useLayoutEffect(() => { useLayoutEffect(() => {
clearSelections(); const newSelections = nodes.filter(node => selectedIDs.includes(Number(node.id))).map(node => node.id);
}, [toggleResetSelection, clearSelections]); setSelections(newSelections);
}, [selectedIDs, setSelections, nodes]);
useLayoutEffect(() => {
setSelected(selections.map(id => Number(id)));
}, [selections, setSelected]);
const canvasWidth = useMemo(() => { const canvasWidth = useMemo(() => {
return 'calc(100vw - 1.1rem)'; return 'calc(100vw - 1.1rem)';

View File

@ -1,10 +1,10 @@
import { useLayoutEffect, useMemo, useState } from 'react'; import { useLayoutEffect, useMemo, useState } from 'react';
import { Graph } from '@/models/Graph';
import { GraphFilterParams } from '@/models/miscellaneous'; import { GraphFilterParams } from '@/models/miscellaneous';
import { CstType, IRSForm } from '@/models/rsform'; import { CstType, IRSForm } from '@/models/rsform';
import { Graph } from '@/models/Graph';
function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams, toggleUpdate: boolean) { function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams) {
const [filtered, setFiltered] = useState<Graph>(new Graph()); const [filtered, setFiltered] = useState<Graph>(new Graph());
const allowedTypes: CstType[] = useMemo(() => { const allowedTypes: CstType[] = useMemo(() => {
@ -47,7 +47,7 @@ function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams,
}); });
} }
setFiltered(graph); setFiltered(graph);
}, [schema, params, allowedTypes, toggleUpdate]); }, [schema, params, allowedTypes]);
return filtered; return filtered;
} }

View File

@ -28,7 +28,7 @@ import DlgRenameCst from '@/dialogs/DlgRenameCst';
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm'; import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
import useQueryStrings from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
import { UserAccessMode } from '@/models/miscellaneous'; import { UserAccessMode } from '@/models/miscellaneous';
import { IConstituenta, ICstCreateData, ICstRenameData, ICstUpdateData, TermForm } from '@/models/rsform'; import { CstType, IConstituenta, ICstCreateData, ICstRenameData, ICstUpdateData, TermForm } from '@/models/rsform';
import { generateAlias } from '@/models/rsformAPI'; import { generateAlias } from '@/models/rsformAPI';
import { EXTEOR_TRS_FILE, prefixes, TIMEOUT_UI_REFRESH } from '@/utils/constants'; import { EXTEOR_TRS_FILE, prefixes, TIMEOUT_UI_REFRESH } from '@/utils/constants';
@ -85,14 +85,18 @@ function RSTabs() {
); );
}, [user?.is_staff, mode, isOwned, loading, processing]); }, [user?.is_staff, mode, isOwned, loading, processing]);
const [activeID, setActiveID] = useState<number | undefined>(undefined); const [selected, setSelected] = useState<number[]>([]);
const activeCst = useMemo(() => schema?.items?.find(cst => cst.id === activeID), [schema?.items, activeID]); const activeCst: IConstituenta | undefined = useMemo(() => {
if (!schema || selected.length === 0) {
return undefined;
} else {
return schema.items.find(cst => cst.id === selected.at(-1));
}
}, [schema, selected]);
const [showUpload, setShowUpload] = useState(false); const [showUpload, setShowUpload] = useState(false);
const [showClone, setShowClone] = useState(false); const [showClone, setShowClone] = useState(false);
const [afterDelete, setAfterDelete] = useState<((items: number[]) => void) | undefined>(undefined);
const [toBeDeleted, setToBeDeleted] = useState<number[]>([]);
const [showDeleteCst, setShowDeleteCst] = useState(false); const [showDeleteCst, setShowDeleteCst] = useState(false);
const [createInitialData, setCreateInitialData] = useState<ICstCreateData>(); const [createInitialData, setCreateInitialData] = useState<ICstCreateData>();
@ -118,10 +122,19 @@ function RSTabs() {
useLayoutEffect(() => { useLayoutEffect(() => {
setNoFooter(activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST); setNoFooter(activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST);
setActiveID(Number(cstQuery) ?? (schema && schema?.items.length > 0 ? schema.items[0].id : undefined));
setIsModified(false); setIsModified(false);
if (activeTab === RSTabID.CST_EDIT) {
const cstID = Number(cstQuery);
if (cstID && schema && schema.items.find(cst => cst.id === cstID)) {
setSelected([cstID]);
} else if (schema && schema?.items.length > 0) {
setSelected([schema.items[0].id]);
} else {
setSelected([]);
}
}
return () => setNoFooter(false); return () => setNoFooter(false);
}, [activeTab, cstQuery, setActiveID, schema, setNoFooter, setIsModified]); }, [activeTab, cstQuery, setSelected, schema, setNoFooter, setIsModified]);
useLayoutEffect( useLayoutEffect(
() => () =>
@ -137,10 +150,6 @@ function RSTabs() {
[schema, setMode, isOwned] [schema, setMode, isOwned]
); );
function onSelectTab(index: number) {
navigateTab(index, activeID);
}
const navigateTab = useCallback( const navigateTab = useCallback(
(tab: RSTabID, activeID?: number) => { (tab: RSTabID, activeID?: number) => {
if (!schema) { if (!schema) {
@ -162,6 +171,10 @@ function RSTabs() {
[router, schema, activeTab] [router, schema, activeTab]
); );
function onSelectTab(index: number) {
navigateTab(index, selected.length > 0 ? selected.at(-1) : undefined);
}
const handleCreateCst = useCallback( const handleCreateCst = useCallback(
(data: ICstCreateData) => { (data: ICstCreateData) => {
if (!schema?.items) { if (!schema?.items) {
@ -189,17 +202,44 @@ function RSTabs() {
); );
const promptCreateCst = useCallback( const promptCreateCst = useCallback(
(initialData: ICstCreateData, skipDialog?: boolean) => { (type: CstType | undefined, skipDialog: boolean, definition?: string) => {
const data: ICstCreateData = {
insert_after: activeCst?.id ?? null,
cst_type: type ?? activeCst?.cst_type ?? CstType.BASE,
alias: '',
term_raw: '',
definition_formal: definition ?? '',
definition_raw: '',
convention: '',
term_forms: []
};
if (skipDialog) { if (skipDialog) {
handleCreateCst(initialData); handleCreateCst(data);
} else { } else {
setCreateInitialData(initialData); setCreateInitialData(data);
setShowCreateCst(true); setShowCreateCst(true);
} }
}, },
[handleCreateCst] [handleCreateCst, activeCst]
); );
const handleCloneCst = useCallback(() => {
if (!activeCst) {
return;
}
const data: ICstCreateData = {
insert_after: activeCst.id,
cst_type: activeCst.cst_type,
alias: '',
term_raw: activeCst.term_raw,
definition_formal: activeCst.definition_formal,
definition_raw: activeCst.definition_raw,
convention: activeCst.convention,
term_forms: activeCst.term_forms
};
handleCreateCst(data);
}, [activeCst, handleCreateCst]);
const handleRenameCst = useCallback( const handleRenameCst = useCallback(
(data: ICstRenameData) => { (data: ICstRenameData) => {
cstRename(data, () => toast.success(`Переименование: ${renameInitialData!.alias} -> ${data.alias}`)); cstRename(data, () => toast.success(`Переименование: ${renameInitialData!.alias} -> ${data.alias}`));
@ -207,10 +247,18 @@ function RSTabs() {
[cstRename, renameInitialData] [cstRename, renameInitialData]
); );
const promptRenameCst = useCallback((initialData: ICstRenameData) => { const promptRenameCst = useCallback(() => {
setRenameInitialData(initialData); if (!activeCst) {
return;
}
const data: ICstRenameData = {
id: activeCst.id,
alias: activeCst.alias,
cst_type: activeCst.cst_type
};
setRenameInitialData(data);
setShowRenameCst(true); setShowRenameCst(true);
}, []); }, [activeCst]);
const onReindex = useCallback(() => resetAliases(() => toast.success('Имена конституент обновлены')), [resetAliases]); const onReindex = useCallback(() => resetAliases(() => toast.success('Имена конституент обновлены')), [resetAliases]);
@ -225,33 +273,26 @@ function RSTabs() {
const deletedNames = deleted.map(id => schema.items.find(cst => cst.id === id)?.alias).join(', '); const deletedNames = deleted.map(id => schema.items.find(cst => cst.id === id)?.alias).join(', ');
const isEmpty = deleted.length === schema.items.length; const isEmpty = deleted.length === schema.items.length;
const nextActive = isEmpty ? undefined : getNextActiveOnDelete(activeID, schema.items, deleted); const nextActive = isEmpty ? undefined : getNextActiveOnDelete(activeCst?.id, schema.items, deleted);
cstDelete(data, () => { cstDelete(data, () => {
toast.success(`Конституенты удалены: ${deletedNames}`); toast.success(`Конституенты удалены: ${deletedNames}`);
if (isEmpty) { if (isEmpty) {
navigateTab(RSTabID.CST_LIST); navigateTab(RSTabID.CST_LIST);
} else if (!nextActive) { } else if (activeTab === RSTabID.CST_EDIT) {
navigateTab(activeTab);
} else {
navigateTab(activeTab, nextActive); navigateTab(activeTab, nextActive);
} else {
setSelected(nextActive ? [nextActive] : []);
navigateTab(activeTab);
} }
if (afterDelete) afterDelete(deleted);
}); });
}, },
[afterDelete, cstDelete, schema, activeID, activeTab, navigateTab] [cstDelete, schema, activeTab, activeCst, navigateTab]
); );
const promptDeleteCst = useCallback((selected: number[], callback?: (items: number[]) => void) => {
setAfterDelete(() => (items: number[]) => {
if (callback) callback(items);
});
setToBeDeleted(selected);
setShowDeleteCst(true);
}, []);
const onOpenCst = useCallback( const onOpenCst = useCallback(
(cstID: number) => { (cstID: number) => {
setSelected([cstID]);
navigateTab(RSTabID.CST_EDIT, cstID); navigateTab(RSTabID.CST_EDIT, cstID);
}, },
[navigateTab] [navigateTab]
@ -334,16 +375,16 @@ function RSTabs() {
const handleSaveWordforms = useCallback( const handleSaveWordforms = useCallback(
(forms: TermForm[]) => { (forms: TermForm[]) => {
if (!activeID) { if (!activeCst) {
return; return;
} }
const data: ICstUpdateData = { const data: ICstUpdateData = {
id: activeID, id: activeCst.id,
term_forms: forms term_forms: forms
}; };
cstUpdate(data, () => toast.success('Изменения сохранены')); cstUpdate(data, () => toast.success('Изменения сохранены'));
}, },
[cstUpdate, activeID] [cstUpdate, activeCst]
); );
return ( return (
@ -371,7 +412,7 @@ function RSTabs() {
schema={schema!} schema={schema!}
hideWindow={() => setShowDeleteCst(false)} hideWindow={() => setShowDeleteCst(false)}
onDelete={handleDeleteCst} onDelete={handleDeleteCst}
selected={toBeDeleted} selected={selected}
/> />
) : null} ) : null}
{showEditTerm ? ( {showEditTerm ? (
@ -438,34 +479,41 @@ function RSTabs() {
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '' : 'none' }}> <TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '' : 'none' }}>
<EditorRSList <EditorRSList
selected={selected}
setSelected={setSelected}
isMutable={isMutable} isMutable={isMutable}
onOpenEdit={onOpenCst} onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst} onClone={handleCloneCst}
onDeleteCst={promptDeleteCst} onCreate={type => promptCreateCst(type, type !== undefined)}
onDelete={() => setShowDeleteCst(true)}
/> />
</TabPanel> </TabPanel>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '' : 'none' }}> <TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '' : 'none' }}>
<EditorConstituenta <EditorConstituenta
schema={schema}
isMutable={isMutable} isMutable={isMutable}
isModified={isModified} isModified={isModified}
setIsModified={setIsModified} setIsModified={setIsModified}
activeID={activeID}
activeCst={activeCst} activeCst={activeCst}
onOpenEdit={onOpenCst} onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst} onClone={handleCloneCst}
onDeleteCst={promptDeleteCst} onCreate={type => promptCreateCst(type, false)}
onRenameCst={promptRenameCst} onDelete={() => setShowDeleteCst(true)}
onRename={promptRenameCst}
onEditTerm={promptShowEditTerm} onEditTerm={promptShowEditTerm}
/> />
</TabPanel> </TabPanel>
<TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '' : 'none' }}> <TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '' : 'none' }}>
<EditorTermGraph <EditorTermGraph
schema={schema}
selected={selected}
setSelected={setSelected}
isMutable={isMutable} isMutable={isMutable}
onOpenEdit={onOpenCst} onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst} onCreate={(type, definition) => promptCreateCst(type, false, definition)}
onDeleteCst={promptDeleteCst} onDelete={() => setShowDeleteCst(true)}
/> />
</TabPanel> </TabPanel>
</AnimateFade> </AnimateFade>

View File

@ -118,6 +118,7 @@ function RSTabsMenu({
dense dense
tabIndex={-1} tabIndex={-1}
title='Меню' title='Меню'
hideTitle={schemaMenu.isOpen}
icon={<BiMenu size='1.25rem' className='clr-text-controls' />} icon={<BiMenu size='1.25rem' className='clr-text-controls' />}
className='h-full pl-2' className='h-full pl-2'
style={{ outlineColor: 'transparent' }} style={{ outlineColor: 'transparent' }}
@ -172,6 +173,7 @@ function RSTabsMenu({
noBorder noBorder
tabIndex={-1} tabIndex={-1}
title={'Редактирование'} title={'Редактирование'}
hideTitle={editMenu.isOpen}
className='h-full' className='h-full'
style={{ outlineColor: 'transparent' }} style={{ outlineColor: 'transparent' }}
icon={<FiEdit size='1.25rem' className={isMutable ? 'clr-text-success' : 'clr-text-warning'} />} icon={<FiEdit size='1.25rem' className={isMutable ? 'clr-text-success' : 'clr-text-warning'} />}
@ -201,6 +203,7 @@ function RSTabsMenu({
noBorder noBorder
tabIndex={-1} tabIndex={-1}
title={`Режим ${labelAccessMode(mode)}`} title={`Режим ${labelAccessMode(mode)}`}
hideTitle={accessMenu.isOpen}
className='h-full pr-2' className='h-full pr-2'
style={{ outlineColor: 'transparent' }} style={{ outlineColor: 'transparent' }}
icon={ icon={

View File

@ -82,6 +82,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
transparent transparent
tabIndex={-1} tabIndex={-1}
title='Настройка атрибутов для фильтрации' title='Настройка атрибутов для фильтрации'
hideTitle={matchModeMenu.isOpen}
className='h-full' className='h-full'
icon={<BiFilterAlt size='1.25rem' />} icon={<BiFilterAlt size='1.25rem' />}
text={labelCstMatchMode(filterMatch)} text={labelCstMatchMode(filterMatch)}
@ -94,6 +95,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
const matchMode = value as CstMatchMode; const matchMode = value as CstMatchMode;
return ( return (
<DropdownButton <DropdownButton
className='w-[22rem]'
key={`${prefixes.cst_match_mode_list}${index}`} key={`${prefixes.cst_match_mode_list}${index}`}
onClick={() => handleMatchModeChange(matchMode)} onClick={() => handleMatchModeChange(matchMode)}
> >
@ -111,6 +113,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
transparent transparent
tabIndex={-1} tabIndex={-1}
title='Настройка фильтрации по графу термов' title='Настройка фильтрации по графу термов'
hideTitle={sourceMenu.isOpen}
className='h-full pr-2' className='h-full pr-2'
icon={<BiCog size='1.25rem' />} icon={<BiCog size='1.25rem' />}
text={labelCstSource(filterSource)} text={labelCstSource(filterSource)}
@ -122,7 +125,11 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
.map((value, index) => { .map((value, index) => {
const source = value as DependencyMode; const source = value as DependencyMode;
return ( return (
<DropdownButton key={`${prefixes.cst_source_list}${index}`} onClick={() => handleSourceChange(source)}> <DropdownButton
className='w-[23rem]'
key={`${prefixes.cst_source_list}${index}`}
onClick={() => handleSourceChange(source)}
>
<p> <p>
<b>{labelCstSource(source)}:</b> {describeCstSource(source)} <b>{labelCstSource(source)}:</b> {describeCstSource(source)}
</p> </p>