mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Refactoring: extract ui controller for RSEdit into context
This commit is contained in:
parent
6ae6451236
commit
9f67e6128b
|
@ -5,9 +5,10 @@ import { useMemo, useState } from 'react';
|
|||
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
import { CstType, IConstituenta, IRSForm } from '@/models/rsform';
|
||||
import { IConstituenta } from '@/models/rsform';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import ViewConstituents from '../ViewConstituents';
|
||||
import ConstituentaToolbar from './ConstituentaToolbar';
|
||||
import FormConstituenta from './FormConstituenta';
|
||||
|
@ -19,48 +20,20 @@ const UNFOLDED_HEIGHT = '59.1rem';
|
|||
const SIDELIST_HIDE_THRESHOLD = 1100; // px
|
||||
|
||||
interface EditorConstituentaProps {
|
||||
schema?: IRSForm;
|
||||
isMutable: boolean;
|
||||
|
||||
activeCst?: IConstituenta;
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
||||
onMoveUp: () => void;
|
||||
onMoveDown: () => void;
|
||||
onOpenEdit: (cstID: number) => void;
|
||||
onClone: () => void;
|
||||
onCreate: (type?: CstType) => void;
|
||||
onRename: () => void;
|
||||
onEditTerm: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
function EditorConstituenta({
|
||||
schema,
|
||||
isMutable,
|
||||
isModified,
|
||||
setIsModified,
|
||||
activeCst,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onOpenEdit,
|
||||
onClone,
|
||||
onCreate,
|
||||
onRename,
|
||||
onEditTerm,
|
||||
onDelete
|
||||
}: EditorConstituentaProps) {
|
||||
function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }: EditorConstituentaProps) {
|
||||
const controller = useRSEdit();
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
const [showList, setShowList] = useLocalStorage('rseditor-show-list', true);
|
||||
const [toggleReset, setToggleReset] = useState(false);
|
||||
|
||||
const disabled = useMemo(() => !activeCst || !isMutable, [activeCst, isMutable]);
|
||||
|
||||
function handleCreate() {
|
||||
onCreate(activeCst?.cst_type);
|
||||
}
|
||||
const disabled = useMemo(() => !activeCst || !controller.isMutable, [activeCst, controller.isMutable]);
|
||||
|
||||
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (disabled) {
|
||||
|
@ -92,7 +65,7 @@ function EditorConstituenta({
|
|||
function processAltKey(code: string): boolean {
|
||||
switch (code) {
|
||||
case 'KeyV':
|
||||
onClone();
|
||||
controller.cloneCst();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -103,13 +76,13 @@ function EditorConstituenta({
|
|||
<ConstituentaToolbar
|
||||
isMutable={!disabled}
|
||||
isModified={isModified}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
onMoveUp={controller.moveUp}
|
||||
onMoveDown={controller.moveDown}
|
||||
onSubmit={initiateSubmit}
|
||||
onReset={() => setToggleReset(prev => !prev)}
|
||||
onDelete={onDelete}
|
||||
onClone={onClone}
|
||||
onCreate={handleCreate}
|
||||
onDelete={controller.deleteCst}
|
||||
onClone={controller.cloneCst}
|
||||
onCreate={() => controller.createCst(activeCst?.cst_type, false)}
|
||||
/>
|
||||
<div tabIndex={-1} className='flex max-w-[95rem]' onKeyDown={handleInput}>
|
||||
<FormConstituenta
|
||||
|
@ -121,13 +94,13 @@ function EditorConstituenta({
|
|||
toggleReset={toggleReset}
|
||||
onToggleList={() => setShowList(prev => !prev)}
|
||||
setIsModified={setIsModified}
|
||||
onEditTerm={onEditTerm}
|
||||
onRename={onRename}
|
||||
onEditTerm={controller.editTermForms}
|
||||
onRename={controller.renameCst}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{showList && windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD ? (
|
||||
<ViewConstituents
|
||||
schema={schema}
|
||||
schema={controller.schema}
|
||||
expression={activeCst?.definition_formal ?? ''}
|
||||
baseHeight={UNFOLDED_HEIGHT}
|
||||
activeID={activeCst?.id}
|
||||
|
|
|
@ -7,32 +7,19 @@ import { useAuth } from '@/context/AuthContext';
|
|||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import FormRSForm from './FormRSForm';
|
||||
import RSFormStats from './RSFormStats';
|
||||
import RSFormToolbar from './RSFormToolbar';
|
||||
|
||||
interface EditorRSFormProps {
|
||||
isModified: boolean;
|
||||
isMutable: boolean;
|
||||
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onDestroy: () => void;
|
||||
onClaim: () => void;
|
||||
onShare: () => void;
|
||||
onDownload: () => void;
|
||||
onToggleSubscribe: () => void;
|
||||
}
|
||||
|
||||
function EditorRSForm({
|
||||
isModified,
|
||||
isMutable,
|
||||
onDestroy,
|
||||
onClaim,
|
||||
onShare,
|
||||
setIsModified,
|
||||
onDownload,
|
||||
onToggleSubscribe
|
||||
}: EditorRSFormProps) {
|
||||
function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProps) {
|
||||
const { isMutable } = useRSEdit();
|
||||
const { schema, isClaimable, isSubscribed, processing } = useRSForm();
|
||||
const { user } = useAuth();
|
||||
|
||||
|
@ -55,18 +42,13 @@ function EditorRSForm({
|
|||
return (
|
||||
<>
|
||||
<RSFormToolbar
|
||||
isMutable={isMutable}
|
||||
processing={processing}
|
||||
isSubscribed={isSubscribed}
|
||||
subscribed={isSubscribed}
|
||||
modified={isModified}
|
||||
claimable={isClaimable}
|
||||
anonymous={!user}
|
||||
onSubmit={initiateSubmit}
|
||||
onShare={onShare}
|
||||
onDownload={onDownload}
|
||||
onClaim={onClaim}
|
||||
onDestroy={onDestroy}
|
||||
onToggleSubscribe={onToggleSubscribe}
|
||||
/>
|
||||
<div tabIndex={-1} className='flex flex-col sm:flex-row w-fit' onKeyDown={handleInput}>
|
||||
<FlexColumn className='px-4 pb-2'>
|
||||
|
|
|
@ -10,37 +10,29 @@ import MiniButton from '@/components/ui/MiniButton';
|
|||
import Overlay from '@/components/ui/Overlay';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
||||
interface RSFormToolbarProps {
|
||||
isMutable: boolean;
|
||||
isSubscribed: boolean;
|
||||
modified: boolean;
|
||||
claimable: boolean;
|
||||
anonymous: boolean;
|
||||
processing: boolean;
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
interface RSFormToolbarProps {
|
||||
modified: boolean;
|
||||
subscribed: boolean;
|
||||
anonymous: boolean;
|
||||
claimable: boolean;
|
||||
processing: boolean;
|
||||
onSubmit: () => void;
|
||||
onShare: () => void;
|
||||
onDownload: () => void;
|
||||
onClaim: () => void;
|
||||
onDestroy: () => void;
|
||||
onToggleSubscribe: () => void;
|
||||
}
|
||||
|
||||
function RSFormToolbar({
|
||||
isMutable,
|
||||
modified,
|
||||
claimable,
|
||||
anonymous,
|
||||
isSubscribed,
|
||||
onToggleSubscribe,
|
||||
subscribed,
|
||||
claimable,
|
||||
processing,
|
||||
onSubmit,
|
||||
onShare,
|
||||
onDownload,
|
||||
onClaim,
|
||||
onDestroy
|
||||
}: RSFormToolbarProps) {
|
||||
const canSave = useMemo(() => modified && isMutable, [modified, isMutable]);
|
||||
const controller = useRSEdit();
|
||||
const canSave = useMemo(() => modified && controller.isMutable, [modified, controller.isMutable]);
|
||||
return (
|
||||
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'>
|
||||
<MiniButton
|
||||
|
@ -52,37 +44,37 @@ function RSFormToolbar({
|
|||
<MiniButton
|
||||
title='Поделиться схемой'
|
||||
icon={<BiShareAlt size='1.25rem' className='clr-text-primary' />}
|
||||
onClick={onShare}
|
||||
onClick={controller.share}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Скачать TRS файл'
|
||||
icon={<BiDownload size='1.25rem' className='clr-text-primary' />}
|
||||
onClick={onDownload}
|
||||
onClick={controller.download}
|
||||
/>
|
||||
<MiniButton
|
||||
title={`Отслеживание ${isSubscribed ? 'включено' : 'выключено'}`}
|
||||
title={`Отслеживание ${subscribed ? 'включено' : 'выключено'}`}
|
||||
disabled={anonymous || processing}
|
||||
icon={
|
||||
isSubscribed ? (
|
||||
subscribed ? (
|
||||
<FiBell size='1.25rem' className='clr-text-primary' />
|
||||
) : (
|
||||
<FiBellOff size='1.25rem' className='clr-text-controls' />
|
||||
)
|
||||
}
|
||||
style={{ outlineColor: 'transparent' }}
|
||||
onClick={onToggleSubscribe}
|
||||
onClick={controller.toggleSubscribe}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Стать владельцем'
|
||||
icon={<LuCrown size='1.25rem' className={!claimable || anonymous ? '' : 'clr-text-success'} />}
|
||||
disabled={!claimable || anonymous || processing}
|
||||
onClick={onClaim}
|
||||
onClick={controller.claim}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Удалить схему'
|
||||
disabled={!isMutable}
|
||||
disabled={!controller.isMutable}
|
||||
onClick={onDestroy}
|
||||
icon={<BiTrash size='1.25rem' className={isMutable ? 'clr-text-warning' : ''} />}
|
||||
icon={<BiTrash size='1.25rem' className={controller.isMutable ? 'clr-text-warning' : ''} />}
|
||||
/>
|
||||
<HelpButton topic={HelpTopic.RSFORM} offset={4} />
|
||||
</Overlay>
|
||||
|
|
|
@ -4,57 +4,41 @@ import { useLayoutEffect, useState } from 'react';
|
|||
|
||||
import { type RowSelectionState } from '@/components/DataTable';
|
||||
import SelectedCounter from '@/components/SelectedCounter';
|
||||
import { CstType, IRSForm } from '@/models/rsform';
|
||||
import { CstType } from '@/models/rsform';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import RSListToolbar from './RSListToolbar';
|
||||
import RSTable from './RSTable';
|
||||
|
||||
interface EditorRSListProps {
|
||||
schema?: IRSForm;
|
||||
isMutable: boolean;
|
||||
selected: number[];
|
||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||
onMoveUp: () => void;
|
||||
onMoveDown: () => void;
|
||||
onOpenEdit: (cstID: number) => void;
|
||||
onClone: () => void;
|
||||
onCreate: (type?: CstType) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
function EditorRSList({
|
||||
schema,
|
||||
selected,
|
||||
setSelected,
|
||||
isMutable,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onOpenEdit,
|
||||
onClone,
|
||||
onCreate,
|
||||
onDelete
|
||||
}: EditorRSListProps) {
|
||||
function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps) {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const controller = useRSEdit();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!schema || selected.length === 0) {
|
||||
if (!controller.schema || selected.length === 0) {
|
||||
setRowSelection({});
|
||||
} else {
|
||||
const newRowSelection: RowSelectionState = {};
|
||||
schema.items.forEach((cst, index) => {
|
||||
controller.schema.items.forEach((cst, index) => {
|
||||
newRowSelection[String(index)] = selected.includes(cst.id);
|
||||
});
|
||||
setRowSelection(newRowSelection);
|
||||
}
|
||||
}, [selected, schema]);
|
||||
}, [selected, controller.schema]);
|
||||
|
||||
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
||||
if (!schema) {
|
||||
if (!controller.schema) {
|
||||
setSelected([]);
|
||||
} else {
|
||||
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||
const newSelection: number[] = [];
|
||||
schema?.items.forEach((cst, index) => {
|
||||
controller.schema.items.forEach((cst, index) => {
|
||||
if (newRowSelection[String(index)] === true) {
|
||||
newSelection.push(cst.id);
|
||||
}
|
||||
|
@ -63,14 +47,13 @@ function EditorRSList({
|
|||
}
|
||||
}
|
||||
|
||||
// Implement hotkeys for working with constituents table
|
||||
function handleTableKey(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (!isMutable) {
|
||||
if (!controller.isMutable) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Delete' && selected.length > 0) {
|
||||
event.preventDefault();
|
||||
onDelete();
|
||||
controller.deleteCst();
|
||||
return;
|
||||
}
|
||||
if (!event.altKey || event.shiftKey) {
|
||||
|
@ -86,23 +69,23 @@ function EditorRSList({
|
|||
if (selected.length > 0) {
|
||||
// prettier-ignore
|
||||
switch (code) {
|
||||
case 'ArrowUp': onMoveUp(); return true;
|
||||
case 'ArrowDown': onMoveDown(); return true;
|
||||
case 'KeyV': onClone(); return true;
|
||||
case 'ArrowUp': controller.moveUp(); return true;
|
||||
case 'ArrowDown': controller.moveDown(); return true;
|
||||
case 'KeyV': controller.cloneCst(); return true;
|
||||
}
|
||||
}
|
||||
// prettier-ignore
|
||||
switch (code) {
|
||||
case 'Backquote': onCreate(); return true;
|
||||
case 'Backquote': controller.createCst(undefined, false); return true;
|
||||
|
||||
case 'Digit1': onCreate(CstType.BASE); return true;
|
||||
case 'Digit2': onCreate(CstType.STRUCTURED); return true;
|
||||
case 'Digit3': onCreate(CstType.TERM); return true;
|
||||
case 'Digit4': onCreate(CstType.AXIOM); return true;
|
||||
case 'KeyQ': onCreate(CstType.FUNCTION); return true;
|
||||
case 'KeyW': onCreate(CstType.PREDICATE); return true;
|
||||
case 'Digit5': onCreate(CstType.CONSTANT); return true;
|
||||
case 'Digit6': onCreate(CstType.THEOREM); return true;
|
||||
case 'Digit1': controller.createCst(CstType.BASE, true); return true;
|
||||
case 'Digit2': controller.createCst(CstType.STRUCTURED, true); return true;
|
||||
case 'Digit3': controller.createCst(CstType.TERM, true); return true;
|
||||
case 'Digit4': controller.createCst(CstType.AXIOM, true); return true;
|
||||
case 'KeyQ': controller.createCst(CstType.FUNCTION, true); return true;
|
||||
case 'KeyW': controller.createCst(CstType.PREDICATE, true); return true;
|
||||
case 'Digit5': controller.createCst(CstType.CONSTANT, true); return true;
|
||||
case 'Digit6': controller.createCst(CstType.THEOREM, true); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -110,29 +93,21 @@ function EditorRSList({
|
|||
return (
|
||||
<div tabIndex={-1} className='outline-none' onKeyDown={handleTableKey}>
|
||||
<SelectedCounter
|
||||
totalCount={schema?.stats?.count_all ?? 0}
|
||||
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||
selectedCount={selected.length}
|
||||
position='top-[0.3rem] left-2'
|
||||
/>
|
||||
|
||||
<RSListToolbar
|
||||
selectedCount={selected.length}
|
||||
isMutable={isMutable}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
onClone={onClone}
|
||||
onCreate={onCreate}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
<RSListToolbar selectedCount={selected.length} />
|
||||
|
||||
<div className='pt-[2.3rem] border-b' />
|
||||
|
||||
<RSTable
|
||||
items={schema?.items}
|
||||
items={controller.schema?.items}
|
||||
selected={rowSelection}
|
||||
setSelected={handleRowSelection}
|
||||
onEdit={onOpenEdit}
|
||||
onCreateNew={onCreate}
|
||||
onCreateNew={() => controller.createCst(undefined, false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -15,26 +15,14 @@ import { getCstTypePrefix } from '@/models/rsformAPI';
|
|||
import { prefixes } from '@/utils/constants';
|
||||
import { getCstTypeShortcut, labelCstType } from '@/utils/labels';
|
||||
|
||||
interface RSListToolbarProps {
|
||||
isMutable?: boolean;
|
||||
selectedCount: number;
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
onMoveUp: () => void;
|
||||
onMoveDown: () => void;
|
||||
onDelete: () => void;
|
||||
onClone: () => void;
|
||||
onCreate: (type?: CstType) => void;
|
||||
interface RSListToolbarProps {
|
||||
selectedCount: number;
|
||||
}
|
||||
|
||||
function RSListToolbar({
|
||||
selectedCount,
|
||||
isMutable,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onDelete,
|
||||
onClone,
|
||||
onCreate
|
||||
}: RSListToolbarProps) {
|
||||
function RSListToolbar({ selectedCount }: RSListToolbarProps) {
|
||||
const controller = useRSEdit();
|
||||
const insertMenu = useDropdown();
|
||||
const nothingSelected = useMemo(() => selectedCount === 0, [selectedCount]);
|
||||
|
||||
|
@ -42,34 +30,43 @@ function RSListToolbar({
|
|||
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex items-start'>
|
||||
<MiniButton
|
||||
title='Переместить вверх [Alt + вверх]'
|
||||
icon={<BiUpvote size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-primary' : ''} />}
|
||||
disabled={!isMutable || nothingSelected}
|
||||
onClick={onMoveUp}
|
||||
icon={
|
||||
<BiUpvote size='1.25rem' className={controller.isMutable && !nothingSelected ? 'clr-text-primary' : ''} />
|
||||
}
|
||||
disabled={!controller.isMutable || nothingSelected}
|
||||
onClick={controller.moveUp}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Переместить вниз [Alt + вниз]'
|
||||
icon={<BiDownvote size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-primary' : ''} />}
|
||||
disabled={!isMutable || nothingSelected}
|
||||
onClick={onMoveDown}
|
||||
icon={
|
||||
<BiDownvote size='1.25rem' className={controller.isMutable && !nothingSelected ? 'clr-text-primary' : ''} />
|
||||
}
|
||||
disabled={!controller.isMutable || nothingSelected}
|
||||
onClick={controller.moveDown}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Клонировать конституенту [Alt + V]'
|
||||
icon={<BiDuplicate size='1.25rem' className={isMutable && selectedCount === 1 ? 'clr-text-success' : ''} />}
|
||||
disabled={!isMutable || selectedCount !== 1}
|
||||
onClick={onClone}
|
||||
icon={
|
||||
<BiDuplicate
|
||||
size='1.25rem'
|
||||
className={controller.isMutable && selectedCount === 1 ? 'clr-text-success' : ''}
|
||||
/>
|
||||
}
|
||||
disabled={!controller.isMutable || selectedCount !== 1}
|
||||
onClick={controller.cloneCst}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Добавить новую конституенту... [Alt + `]'
|
||||
icon={<BiPlusCircle size='1.25rem' className={isMutable ? 'clr-text-success' : ''} />}
|
||||
disabled={!isMutable}
|
||||
onClick={() => onCreate()}
|
||||
icon={<BiPlusCircle size='1.25rem' className={controller.isMutable ? 'clr-text-success' : ''} />}
|
||||
disabled={!controller.isMutable}
|
||||
onClick={() => controller.createCst(undefined, false)}
|
||||
/>
|
||||
<div ref={insertMenu.ref}>
|
||||
<MiniButton
|
||||
title='Добавить пустую конституенту'
|
||||
hideTitle={insertMenu.isOpen}
|
||||
icon={<BiDownArrowCircle size='1.25rem' className={isMutable ? 'clr-text-success' : ''} />}
|
||||
disabled={!isMutable}
|
||||
icon={<BiDownArrowCircle size='1.25rem' className={controller.isMutable ? 'clr-text-success' : ''} />}
|
||||
disabled={!controller.isMutable}
|
||||
onClick={insertMenu.toggle}
|
||||
/>
|
||||
<Dropdown isOpen={insertMenu.isOpen}>
|
||||
|
@ -77,7 +74,7 @@ function RSListToolbar({
|
|||
<DropdownButton
|
||||
key={`${prefixes.csttype_list}${typeStr}`}
|
||||
text={`${getCstTypePrefix(typeStr as CstType)}1 — ${labelCstType(typeStr as CstType)}`}
|
||||
onClick={() => onCreate(typeStr as CstType)}
|
||||
onClick={() => controller.createCst(typeStr as CstType, true)}
|
||||
title={getCstTypeShortcut(typeStr as CstType)}
|
||||
/>
|
||||
))}
|
||||
|
@ -85,9 +82,9 @@ function RSListToolbar({
|
|||
</div>
|
||||
<MiniButton
|
||||
title='Удалить выбранные [Delete]'
|
||||
icon={<BiTrash size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-warning' : ''} />}
|
||||
disabled={!isMutable || nothingSelected}
|
||||
onClick={onDelete}
|
||||
icon={<BiTrash size='1.25rem' className={controller.isMutable && !nothingSelected ? 'clr-text-warning' : ''} />}
|
||||
disabled={!controller.isMutable || nothingSelected}
|
||||
onClick={controller.deleteCst}
|
||||
/>
|
||||
<HelpButton topic={HelpTopic.CSTLIST} offset={5} />
|
||||
</Overlay>
|
||||
|
|
|
@ -12,10 +12,11 @@ import { useConceptTheme } from '@/context/ThemeContext';
|
|||
import DlgGraphParams from '@/dialogs/DlgGraphParams';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { GraphColoringScheme, GraphFilterParams } from '@/models/miscellaneous';
|
||||
import { CstType, IRSForm } from '@/models/rsform';
|
||||
import { CstType } from '@/models/rsform';
|
||||
import { colorBgGraphNode } from '@/styling/color';
|
||||
import { classnames, TIMEOUT_GRAPH_REFRESH } from '@/utils/constants';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import GraphSidebar from './GraphSidebar';
|
||||
import GraphToolbar from './GraphToolbar';
|
||||
import TermGraph from './TermGraph';
|
||||
|
@ -23,24 +24,13 @@ import useGraphFilter from './useGraphFilter';
|
|||
import ViewHidden from './ViewHidden';
|
||||
|
||||
interface EditorTermGraphProps {
|
||||
isMutable: boolean;
|
||||
selected: number[];
|
||||
schema?: IRSForm;
|
||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||
onOpenEdit: (cstID: number) => void;
|
||||
onCreate: (type: CstType, definition: string) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
function EditorTermGraph({
|
||||
schema,
|
||||
selected,
|
||||
setSelected,
|
||||
isMutable,
|
||||
onOpenEdit,
|
||||
onCreate,
|
||||
onDelete
|
||||
}: EditorTermGraphProps) {
|
||||
function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphProps) {
|
||||
const controller = useRSEdit();
|
||||
const { colors } = useConceptTheme();
|
||||
|
||||
const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>('graph_filter', {
|
||||
|
@ -59,7 +49,7 @@ function EditorTermGraph({
|
|||
allowTheorem: true
|
||||
});
|
||||
const [showParamsDialog, setShowParamsDialog] = useState(false);
|
||||
const filtered = useGraphFilter(schema, filterParams);
|
||||
const filtered = useGraphFilter(controller.schema, filterParams);
|
||||
|
||||
const [hidden, setHidden] = useState<number[]>([]);
|
||||
|
||||
|
@ -72,32 +62,32 @@ function EditorTermGraph({
|
|||
|
||||
const [hoverID, setHoverID] = useState<number | undefined>(undefined);
|
||||
const hoverCst = useMemo(() => {
|
||||
return schema?.items.find(cst => cst.id === hoverID);
|
||||
}, [schema?.items, hoverID]);
|
||||
return controller.schema?.items.find(cst => cst.id === hoverID);
|
||||
}, [controller.schema?.items, hoverID]);
|
||||
|
||||
const [toggleResetView, setToggleResetView] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!schema) {
|
||||
if (!controller.schema) {
|
||||
return;
|
||||
}
|
||||
const newDismissed: number[] = [];
|
||||
schema.items.forEach(cst => {
|
||||
controller.schema.items.forEach(cst => {
|
||||
if (!filtered.nodes.has(cst.id)) {
|
||||
newDismissed.push(cst.id);
|
||||
}
|
||||
});
|
||||
setHidden(newDismissed);
|
||||
setHoverID(undefined);
|
||||
}, [schema, filtered]);
|
||||
}, [controller.schema, filtered]);
|
||||
|
||||
const nodes: GraphNode[] = useMemo(() => {
|
||||
const result: GraphNode[] = [];
|
||||
if (!schema) {
|
||||
if (!controller.schema) {
|
||||
return result;
|
||||
}
|
||||
filtered.nodes.forEach(node => {
|
||||
const cst = schema.items.find(cst => cst.id === node.id);
|
||||
const cst = controller.schema!.items.find(cst => cst.id === node.id);
|
||||
if (cst) {
|
||||
result.push({
|
||||
id: String(node.id),
|
||||
|
@ -107,7 +97,7 @@ function EditorTermGraph({
|
|||
}
|
||||
});
|
||||
return result;
|
||||
}, [schema, coloringScheme, filtered.nodes, filterParams.noText, colors]);
|
||||
}, [controller.schema, coloringScheme, filtered.nodes, filterParams.noText, colors]);
|
||||
|
||||
const edges: GraphEdge[] = useMemo(() => {
|
||||
const result: GraphEdge[] = [];
|
||||
|
@ -143,18 +133,18 @@ function EditorTermGraph({
|
|||
}
|
||||
|
||||
function handleCreateCst() {
|
||||
if (!schema) {
|
||||
if (!controller.schema) {
|
||||
return;
|
||||
}
|
||||
const definition = selected.map(id => schema.items.find(cst => cst.id === id)!.alias).join(' ');
|
||||
onCreate(selected.length === 0 ? CstType.BASE : CstType.TERM, definition);
|
||||
const definition = selected.map(id => controller.schema!.items.find(cst => cst.id === id)!.alias).join(' ');
|
||||
controller.createCst(selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
||||
}
|
||||
|
||||
function handleDeleteCst() {
|
||||
if (!schema || selected.length === 0) {
|
||||
if (!controller.schema || selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
onDelete();
|
||||
controller.deleteCst();
|
||||
}
|
||||
|
||||
function handleChangeLayout(newLayout: LayoutTypes) {
|
||||
|
@ -176,7 +166,7 @@ function EditorTermGraph({
|
|||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
// Hotkeys implementation
|
||||
if (!isMutable) {
|
||||
if (!controller.isMutable) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Delete') {
|
||||
|
@ -199,13 +189,13 @@ function EditorTermGraph({
|
|||
|
||||
<SelectedCounter
|
||||
hideZero
|
||||
totalCount={schema?.stats?.count_all ?? 0}
|
||||
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||
selectedCount={selected.length}
|
||||
position='top-[0.3rem] left-0'
|
||||
/>
|
||||
|
||||
<GraphToolbar
|
||||
isMutable={isMutable}
|
||||
isMutable={controller.isMutable}
|
||||
nothingSelected={nothingSelected}
|
||||
is3D={is3D}
|
||||
orbit={orbit}
|
||||
|
@ -243,7 +233,7 @@ function EditorTermGraph({
|
|||
<ViewHidden
|
||||
items={hidden}
|
||||
selected={selected}
|
||||
schema={schema!}
|
||||
schema={controller.schema}
|
||||
coloringScheme={coloringScheme}
|
||||
toggleSelection={toggleDismissed}
|
||||
onEdit={onOpenEdit}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { prefixes } from '@/utils/constants';
|
|||
interface ViewHiddenProps {
|
||||
items: number[];
|
||||
selected: number[];
|
||||
schema: IRSForm;
|
||||
schema?: IRSForm;
|
||||
coloringScheme: GraphColoringScheme;
|
||||
|
||||
toggleSelection: (cstID: number) => void;
|
||||
|
@ -43,7 +43,7 @@ function ViewHidden({ items, selected, toggleSelection, schema, coloringScheme,
|
|||
</p>
|
||||
<div className='flex flex-wrap justify-center gap-2 py-2 overflow-y-auto' style={{ maxHeight: dismissedHeight }}>
|
||||
{items.map(cstID => {
|
||||
const cst = schema.items.find(cst => cst.id === cstID)!;
|
||||
const cst = schema!.items.find(cst => cst.id === cstID)!;
|
||||
const adjustedColoring = coloringScheme === 'none' ? 'status' : coloringScheme;
|
||||
const id = `${prefixes.cst_hidden_list}${cst.alias}`;
|
||||
return (
|
||||
|
|
466
rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx
Normal file
466
rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx
Normal file
|
@ -0,0 +1,466 @@
|
|||
'use client';
|
||||
|
||||
import axios from 'axios';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import fileDownload from 'js-file-download';
|
||||
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import InfoError, { ErrorData } from '@/components/InfoError';
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import TextURL from '@/components/ui/TextURL';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem';
|
||||
import DlgConstituentaTemplate from '@/dialogs/DlgConstituentaTemplate';
|
||||
import DlgCreateCst from '@/dialogs/DlgCreateCst';
|
||||
import DlgDeleteCst from '@/dialogs/DlgDeleteCst';
|
||||
import DlgEditWordForms from '@/dialogs/DlgEditWordForms';
|
||||
import DlgRenameCst from '@/dialogs/DlgRenameCst';
|
||||
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
|
||||
import { UserAccessMode } from '@/models/miscellaneous';
|
||||
import {
|
||||
CstType,
|
||||
IConstituenta,
|
||||
IConstituentaMeta,
|
||||
ICstCreateData,
|
||||
ICstMovetoData,
|
||||
ICstRenameData,
|
||||
ICstUpdateData,
|
||||
IRSForm,
|
||||
TermForm
|
||||
} from '@/models/rsform';
|
||||
import { generateAlias } from '@/models/rsformAPI';
|
||||
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
||||
|
||||
interface IRSEditContext {
|
||||
schema?: IRSForm;
|
||||
isMutable: boolean;
|
||||
|
||||
moveUp: () => void;
|
||||
moveDown: () => void;
|
||||
createCst: (type: CstType | undefined, skipDialog: boolean, definition?: string) => void;
|
||||
renameCst: () => void;
|
||||
cloneCst: () => void;
|
||||
deleteCst: () => void;
|
||||
editTermForms: () => void;
|
||||
|
||||
promptTemplate: () => void;
|
||||
promptClone: () => void;
|
||||
promptUpload: () => void;
|
||||
claim: () => void;
|
||||
share: () => void;
|
||||
toggleSubscribe: () => void;
|
||||
download: () => void;
|
||||
reindex: () => void;
|
||||
}
|
||||
|
||||
const RSEditContext = createContext<IRSEditContext | null>(null);
|
||||
export const useRSEdit = () => {
|
||||
const context = useContext(RSEditContext);
|
||||
if (context === null) {
|
||||
throw new Error('useRSEdit has to be used within <RSEditState.Provider>');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface RSEditStateProps {
|
||||
selected: number[];
|
||||
isModified: boolean;
|
||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||
activeCst?: IConstituenta;
|
||||
|
||||
onCreateCst?: (newCst: IConstituentaMeta) => void;
|
||||
onDeleteCst?: (newActive?: number) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const RSEditState = ({
|
||||
selected,
|
||||
setSelected,
|
||||
activeCst,
|
||||
isModified,
|
||||
onCreateCst,
|
||||
onDeleteCst,
|
||||
children
|
||||
}: RSEditStateProps) => {
|
||||
const { user } = useAuth();
|
||||
const { mode, setMode } = useAccessMode();
|
||||
const model = useRSForm();
|
||||
|
||||
const isMutable = useMemo(() => {
|
||||
return (
|
||||
!model.loading &&
|
||||
!model.processing &&
|
||||
mode !== UserAccessMode.READER &&
|
||||
((model.isOwned || (mode === UserAccessMode.ADMIN && user?.is_staff)) ?? false)
|
||||
);
|
||||
}, [user?.is_staff, mode, model.isOwned, model.loading, model.processing]);
|
||||
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [showClone, setShowClone] = useState(false);
|
||||
|
||||
const [showDeleteCst, setShowDeleteCst] = useState(false);
|
||||
|
||||
const [createInitialData, setCreateInitialData] = useState<ICstCreateData>();
|
||||
const [showCreateCst, setShowCreateCst] = useState(false);
|
||||
|
||||
const [renameInitialData, setRenameInitialData] = useState<ICstRenameData>();
|
||||
const [showRenameCst, setShowRenameCst] = useState(false);
|
||||
|
||||
const [showEditTerm, setShowEditTerm] = useState(false);
|
||||
|
||||
const [insertCstID, setInsertCstID] = useState<number | undefined>(undefined);
|
||||
const [showTemplates, setShowTemplates] = useState(false);
|
||||
|
||||
useLayoutEffect(
|
||||
() =>
|
||||
setMode(prev => {
|
||||
if (prev === UserAccessMode.ADMIN) {
|
||||
return prev;
|
||||
} else if (model.isOwned) {
|
||||
return UserAccessMode.OWNER;
|
||||
} else {
|
||||
return UserAccessMode.READER;
|
||||
}
|
||||
}),
|
||||
[model.schema, setMode, model.isOwned]
|
||||
);
|
||||
|
||||
const handleCreateCst = useCallback(
|
||||
(data: ICstCreateData) => {
|
||||
if (!model.schema) {
|
||||
return;
|
||||
}
|
||||
data.alias = data.alias || generateAlias(data.cst_type, model.schema);
|
||||
model.cstCreate(data, newCst => {
|
||||
toast.success(`Конституента добавлена: ${newCst.alias}`);
|
||||
setSelected([newCst.id]);
|
||||
if (onCreateCst) onCreateCst(newCst);
|
||||
});
|
||||
},
|
||||
[model, setSelected, onCreateCst]
|
||||
);
|
||||
|
||||
const handleRenameCst = useCallback(
|
||||
(data: ICstRenameData) => {
|
||||
model.cstRename(data, () => toast.success(`Переименование: ${renameInitialData!.alias} -> ${data.alias}`));
|
||||
},
|
||||
[model, renameInitialData]
|
||||
);
|
||||
|
||||
const handleDeleteCst = useCallback(
|
||||
(deleted: number[]) => {
|
||||
if (!model.schema) {
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
items: deleted
|
||||
};
|
||||
|
||||
const deletedNames = deleted.map(id => model.schema?.items.find(cst => cst.id === id)?.alias).join(', ');
|
||||
const isEmpty = deleted.length === model.schema.items.length;
|
||||
const nextActive = isEmpty ? undefined : getNextActiveOnDelete(activeCst?.id, model.schema.items, deleted);
|
||||
|
||||
model.cstDelete(data, () => {
|
||||
toast.success(`Конституенты удалены: ${deletedNames}`);
|
||||
setSelected(nextActive ? [nextActive] : []);
|
||||
if (onDeleteCst) onDeleteCst(nextActive);
|
||||
});
|
||||
},
|
||||
[model, activeCst, onDeleteCst, setSelected]
|
||||
);
|
||||
|
||||
const handleSaveWordforms = useCallback(
|
||||
(forms: TermForm[]) => {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
const data: ICstUpdateData = {
|
||||
id: activeCst.id,
|
||||
term_forms: forms
|
||||
};
|
||||
model.cstUpdate(data, () => toast.success('Изменения сохранены'));
|
||||
},
|
||||
[model, activeCst]
|
||||
);
|
||||
|
||||
const moveUp = useCallback(() => {
|
||||
if (!model.schema?.items || selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = model.schema.items.reduce((prev, cst, index) => {
|
||||
if (!selected.includes(cst.id)) {
|
||||
return prev;
|
||||
} else if (prev === -1) {
|
||||
return index;
|
||||
}
|
||||
return Math.min(prev, index);
|
||||
}, -1);
|
||||
const target = Math.max(0, currentIndex - 1) + 1;
|
||||
const data = {
|
||||
items: selected,
|
||||
move_to: target
|
||||
};
|
||||
model.cstMoveTo(data);
|
||||
}, [model, selected]);
|
||||
|
||||
const moveDown = useCallback(() => {
|
||||
if (!model.schema?.items || selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
const currentIndex = model.schema.items.reduce((prev, cst, index) => {
|
||||
if (!selected.includes(cst.id)) {
|
||||
return prev;
|
||||
} else {
|
||||
count += 1;
|
||||
if (prev === -1) {
|
||||
return index;
|
||||
}
|
||||
return Math.max(prev, index);
|
||||
}
|
||||
}, -1);
|
||||
const target = Math.min(model.schema.items.length - 1, currentIndex - count + 2) + 1;
|
||||
const data: ICstMovetoData = {
|
||||
items: selected,
|
||||
move_to: target
|
||||
};
|
||||
model.cstMoveTo(data);
|
||||
}, [model, selected]);
|
||||
|
||||
const createCst = useCallback(
|
||||
(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) {
|
||||
handleCreateCst(data);
|
||||
} else {
|
||||
setCreateInitialData(data);
|
||||
setShowCreateCst(true);
|
||||
}
|
||||
},
|
||||
[activeCst, handleCreateCst]
|
||||
);
|
||||
|
||||
const cloneCst = 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 renameCst = useCallback(() => {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
const data: ICstRenameData = {
|
||||
id: activeCst.id,
|
||||
alias: activeCst.alias,
|
||||
cst_type: activeCst.cst_type
|
||||
};
|
||||
setRenameInitialData(data);
|
||||
setShowRenameCst(true);
|
||||
}, [activeCst]);
|
||||
|
||||
const editTermForms = useCallback(() => {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
if (isModified) {
|
||||
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setShowEditTerm(true);
|
||||
}, [isModified, activeCst]);
|
||||
|
||||
const reindex = useCallback(() => model.resetAliases(() => toast.success('Имена конституент обновлены')), [model]);
|
||||
|
||||
const promptTemplate = useCallback(() => {
|
||||
setInsertCstID(activeCst?.id);
|
||||
setShowTemplates(true);
|
||||
}, [activeCst]);
|
||||
|
||||
const promptClone = useCallback(() => {
|
||||
if (isModified) {
|
||||
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setShowClone(true);
|
||||
}, [isModified]);
|
||||
|
||||
const download = useCallback(() => {
|
||||
if (isModified) {
|
||||
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const fileName = (model.schema?.alias ?? 'Schema') + EXTEOR_TRS_FILE;
|
||||
model.download((data: Blob) => {
|
||||
try {
|
||||
fileDownload(data, fileName);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}, [model, isModified]);
|
||||
|
||||
const claim = useCallback(() => {
|
||||
if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
|
||||
return;
|
||||
}
|
||||
model.claim(() => toast.success('Вы стали владельцем схемы'));
|
||||
}, [model]);
|
||||
|
||||
const share = useCallback(() => {
|
||||
const url = window.location.href + '&share';
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => toast.success(`Ссылка скопирована: ${url}`))
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const toggleSubscribe = useCallback(() => {
|
||||
if (model.isSubscribed) {
|
||||
model.unsubscribe(() => toast.success('Отслеживание отключено'));
|
||||
} else {
|
||||
model.subscribe(() => toast.success('Отслеживание включено'));
|
||||
}
|
||||
}, [model]);
|
||||
|
||||
return (
|
||||
<RSEditContext.Provider
|
||||
value={{
|
||||
schema: model.schema,
|
||||
isMutable,
|
||||
|
||||
moveUp,
|
||||
moveDown,
|
||||
createCst,
|
||||
cloneCst,
|
||||
renameCst,
|
||||
deleteCst: () => setShowDeleteCst(true),
|
||||
editTermForms,
|
||||
|
||||
promptTemplate,
|
||||
promptClone,
|
||||
promptUpload: () => setShowUpload(true),
|
||||
download,
|
||||
claim,
|
||||
share,
|
||||
toggleSubscribe,
|
||||
reindex
|
||||
}}
|
||||
>
|
||||
{model.schema ? (
|
||||
<AnimatePresence>
|
||||
{showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null}
|
||||
{showClone ? <DlgCloneLibraryItem base={model.schema} hideWindow={() => setShowClone(false)} /> : null}
|
||||
{showCreateCst ? (
|
||||
<DlgCreateCst
|
||||
hideWindow={() => setShowCreateCst(false)}
|
||||
onCreate={handleCreateCst}
|
||||
schema={model.schema}
|
||||
initial={createInitialData}
|
||||
/>
|
||||
) : null}
|
||||
{showRenameCst && renameInitialData ? (
|
||||
<DlgRenameCst
|
||||
hideWindow={() => setShowRenameCst(false)}
|
||||
onRename={handleRenameCst}
|
||||
initial={renameInitialData}
|
||||
/>
|
||||
) : null}
|
||||
{showDeleteCst ? (
|
||||
<DlgDeleteCst
|
||||
schema={model.schema}
|
||||
hideWindow={() => setShowDeleteCst(false)}
|
||||
onDelete={handleDeleteCst}
|
||||
selected={selected}
|
||||
/>
|
||||
) : null}
|
||||
{showEditTerm && activeCst ? (
|
||||
<DlgEditWordForms
|
||||
hideWindow={() => setShowEditTerm(false)}
|
||||
onSave={handleSaveWordforms}
|
||||
target={activeCst}
|
||||
/>
|
||||
) : null}
|
||||
{showTemplates ? (
|
||||
<DlgConstituentaTemplate
|
||||
schema={model.schema}
|
||||
hideWindow={() => setShowTemplates(false)}
|
||||
insertAfter={insertCstID}
|
||||
onCreate={handleCreateCst}
|
||||
/>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
) : null}
|
||||
|
||||
{model.loading ? <Loader /> : null}
|
||||
{model.error ? <ProcessError error={model.error} /> : null}
|
||||
{model.schema && !model.loading ? children : null}
|
||||
</RSEditContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// ====== Internals =========
|
||||
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
||||
if (axios.isAxiosError(error) && error.response && error.response.status === 404) {
|
||||
return (
|
||||
<div className='p-2 text-center'>
|
||||
<p>Схема с указанным идентификатором отсутствует на портале.</p>
|
||||
<TextURL text='Перейти в Библиотеку' href='/library' />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <InfoError error={error} />;
|
||||
}
|
||||
}
|
||||
|
||||
function getNextActiveOnDelete(
|
||||
activeID: number | undefined,
|
||||
items: IConstituenta[],
|
||||
deleted: number[]
|
||||
): number | undefined {
|
||||
if (items.length === deleted.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let activeIndex = items.findIndex(cst => cst.id === activeID);
|
||||
if (activeIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
while (activeIndex < items.length && deleted.find(id => id === items[activeIndex].id)) {
|
||||
++activeIndex;
|
||||
}
|
||||
if (activeIndex >= items.length) {
|
||||
activeIndex = items.length - 1;
|
||||
while (activeIndex >= 0 && deleted.find(id => id === items[activeIndex].id)) {
|
||||
--activeIndex;
|
||||
}
|
||||
}
|
||||
return items[activeIndex].id;
|
||||
}
|
|
@ -1,49 +1,25 @@
|
|||
'use client';
|
||||
|
||||
import axios from 'axios';
|
||||
import clsx from 'clsx';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import fileDownload from 'js-file-download';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AnimateFade from '@/components/AnimateFade';
|
||||
import InfoError, { ErrorData } from '@/components/InfoError';
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import TabLabel from '@/components/ui/TabLabel';
|
||||
import TextURL from '@/components/ui/TextURL';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem';
|
||||
import DlgConstituentaTemplate from '@/dialogs/DlgConstituentaTemplate';
|
||||
import DlgCreateCst from '@/dialogs/DlgCreateCst';
|
||||
import DlgDeleteCst from '@/dialogs/DlgDeleteCst';
|
||||
import DlgEditWordForms from '@/dialogs/DlgEditWordForms';
|
||||
import DlgRenameCst from '@/dialogs/DlgRenameCst';
|
||||
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
|
||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||
import { UserAccessMode } from '@/models/miscellaneous';
|
||||
import {
|
||||
CstType,
|
||||
IConstituenta,
|
||||
ICstCreateData,
|
||||
ICstMovetoData,
|
||||
ICstRenameData,
|
||||
ICstUpdateData,
|
||||
TermForm
|
||||
} from '@/models/rsform';
|
||||
import { generateAlias } from '@/models/rsformAPI';
|
||||
import { EXTEOR_TRS_FILE, prefixes, TIMEOUT_UI_REFRESH } from '@/utils/constants';
|
||||
import { IConstituenta, IConstituentaMeta } from '@/models/rsform';
|
||||
import { prefixes, TIMEOUT_UI_REFRESH } from '@/utils/constants';
|
||||
|
||||
import EditorConstituenta from './EditorConstituenta';
|
||||
import EditorRSForm from './EditorRSForm';
|
||||
import EditorRSList from './EditorRSList';
|
||||
import EditorTermGraph from './EditorTermGraph';
|
||||
import { RSEditState } from './RSEditContext';
|
||||
import RSTabsMenu from './RSTabsMenu';
|
||||
|
||||
export enum RSTabID {
|
||||
|
@ -59,41 +35,13 @@ function RSTabs() {
|
|||
const activeTab = (Number(query.get('tab')) ?? RSTabID.CARD) as RSTabID;
|
||||
const cstQuery = query.get('active');
|
||||
|
||||
const {
|
||||
error,
|
||||
schema,
|
||||
loading,
|
||||
processing,
|
||||
isOwned,
|
||||
claim,
|
||||
download,
|
||||
isSubscribed,
|
||||
cstCreate,
|
||||
cstDelete,
|
||||
cstRename,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
cstUpdate,
|
||||
cstMoveTo,
|
||||
resetAliases
|
||||
} = useRSForm();
|
||||
const { schema, loading } = useRSForm();
|
||||
const { destroyItem } = useLibrary();
|
||||
const { setNoFooter } = useConceptTheme();
|
||||
const { user } = useAuth();
|
||||
const { mode, setMode } = useAccessMode();
|
||||
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
useBlockNavigation(isModified);
|
||||
|
||||
const isMutable = useMemo(() => {
|
||||
return (
|
||||
!loading &&
|
||||
!processing &&
|
||||
mode !== UserAccessMode.READER &&
|
||||
((isOwned || (mode === UserAccessMode.ADMIN && user?.is_staff)) ?? false)
|
||||
);
|
||||
}, [user?.is_staff, mode, isOwned, loading, processing]);
|
||||
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const activeCst: IConstituenta | undefined = useMemo(() => {
|
||||
if (!schema || selected.length === 0) {
|
||||
|
@ -103,22 +51,6 @@ function RSTabs() {
|
|||
}
|
||||
}, [schema, selected]);
|
||||
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [showClone, setShowClone] = useState(false);
|
||||
|
||||
const [showDeleteCst, setShowDeleteCst] = useState(false);
|
||||
|
||||
const [createInitialData, setCreateInitialData] = useState<ICstCreateData>();
|
||||
const [showCreateCst, setShowCreateCst] = useState(false);
|
||||
|
||||
const [renameInitialData, setRenameInitialData] = useState<ICstRenameData>();
|
||||
const [showRenameCst, setShowRenameCst] = useState(false);
|
||||
|
||||
const [showEditTerm, setShowEditTerm] = useState(false);
|
||||
|
||||
const [insertCstID, setInsertCstID] = useState<number | undefined>(undefined);
|
||||
const [showTemplates, setShowTemplates] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (schema) {
|
||||
const oldTitle = document.title;
|
||||
|
@ -145,20 +77,6 @@ function RSTabs() {
|
|||
return () => setNoFooter(false);
|
||||
}, [activeTab, cstQuery, setSelected, schema, setNoFooter, setIsModified]);
|
||||
|
||||
useLayoutEffect(
|
||||
() =>
|
||||
setMode(prev => {
|
||||
if (prev === UserAccessMode.ADMIN) {
|
||||
return prev;
|
||||
} else if (isOwned) {
|
||||
return UserAccessMode.OWNER;
|
||||
} else {
|
||||
return UserAccessMode.READER;
|
||||
}
|
||||
}),
|
||||
[schema, setMode, isOwned]
|
||||
);
|
||||
|
||||
const navigateTab = useCallback(
|
||||
(tab: RSTabID, activeID?: number) => {
|
||||
if (!schema) {
|
||||
|
@ -184,166 +102,36 @@ function RSTabs() {
|
|||
navigateTab(index, selected.length > 0 ? selected.at(-1) : undefined);
|
||||
}
|
||||
|
||||
const handleCreateCst = useCallback(
|
||||
(data: ICstCreateData) => {
|
||||
if (!schema?.items) {
|
||||
return;
|
||||
const onCreateCst = useCallback(
|
||||
(newCst: IConstituentaMeta) => {
|
||||
navigateTab(activeTab, newCst.id);
|
||||
if (activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST) {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'nearest'
|
||||
});
|
||||
}
|
||||
}, TIMEOUT_UI_REFRESH);
|
||||
}
|
||||
data.alias = data.alias || generateAlias(data.cst_type, schema);
|
||||
cstCreate(data, newCst => {
|
||||
toast.success(`Конституента добавлена: ${newCst.alias}`);
|
||||
setSelected([newCst.id]);
|
||||
navigateTab(activeTab, newCst.id);
|
||||
if (activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST) {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'nearest'
|
||||
});
|
||||
}
|
||||
}, TIMEOUT_UI_REFRESH);
|
||||
}
|
||||
});
|
||||
},
|
||||
[schema, cstCreate, navigateTab, activeTab]
|
||||
[activeTab, navigateTab]
|
||||
);
|
||||
|
||||
const promptCreateCst = useCallback(
|
||||
(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) {
|
||||
handleCreateCst(data);
|
||||
const onDeleteCst = useCallback(
|
||||
(newActive?: number) => {
|
||||
if (!newActive) {
|
||||
navigateTab(RSTabID.CST_LIST);
|
||||
} else if (activeTab === RSTabID.CST_EDIT) {
|
||||
navigateTab(activeTab, newActive);
|
||||
} else {
|
||||
setCreateInitialData(data);
|
||||
setShowCreateCst(true);
|
||||
navigateTab(activeTab);
|
||||
}
|
||||
},
|
||||
[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(
|
||||
(data: ICstRenameData) => {
|
||||
cstRename(data, () => toast.success(`Переименование: ${renameInitialData!.alias} -> ${data.alias}`));
|
||||
},
|
||||
[cstRename, renameInitialData]
|
||||
);
|
||||
|
||||
const promptRenameCst = useCallback(() => {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
const data: ICstRenameData = {
|
||||
id: activeCst.id,
|
||||
alias: activeCst.alias,
|
||||
cst_type: activeCst.cst_type
|
||||
};
|
||||
setRenameInitialData(data);
|
||||
setShowRenameCst(true);
|
||||
}, [activeCst]);
|
||||
|
||||
const onReindex = useCallback(() => resetAliases(() => toast.success('Имена конституент обновлены')), [resetAliases]);
|
||||
|
||||
// Move selected cst up
|
||||
function handleMoveUp() {
|
||||
if (!schema?.items || selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||
if (!selected.includes(cst.id)) {
|
||||
return prev;
|
||||
} else if (prev === -1) {
|
||||
return index;
|
||||
}
|
||||
return Math.min(prev, index);
|
||||
}, -1);
|
||||
const target = Math.max(0, currentIndex - 1) + 1;
|
||||
const data = {
|
||||
items: selected,
|
||||
move_to: target
|
||||
};
|
||||
cstMoveTo(data);
|
||||
}
|
||||
|
||||
// Move selected cst down
|
||||
function handleMoveDown() {
|
||||
if (!schema?.items || selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||
if (!selected.includes(cst.id)) {
|
||||
return prev;
|
||||
} else {
|
||||
count += 1;
|
||||
if (prev === -1) {
|
||||
return index;
|
||||
}
|
||||
return Math.max(prev, index);
|
||||
}
|
||||
}, -1);
|
||||
const target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1;
|
||||
const data: ICstMovetoData = {
|
||||
items: selected,
|
||||
move_to: target
|
||||
};
|
||||
cstMoveTo(data);
|
||||
}
|
||||
|
||||
const handleDeleteCst = useCallback(
|
||||
(deleted: number[]) => {
|
||||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
items: deleted
|
||||
};
|
||||
|
||||
const deletedNames = deleted.map(id => schema.items.find(cst => cst.id === id)?.alias).join(', ');
|
||||
const isEmpty = deleted.length === schema.items.length;
|
||||
const nextActive = isEmpty ? undefined : getNextActiveOnDelete(activeCst?.id, schema.items, deleted);
|
||||
|
||||
cstDelete(data, () => {
|
||||
toast.success(`Конституенты удалены: ${deletedNames}`);
|
||||
if (isEmpty) {
|
||||
navigateTab(RSTabID.CST_LIST);
|
||||
} else if (activeTab === RSTabID.CST_EDIT) {
|
||||
navigateTab(activeTab, nextActive);
|
||||
} else {
|
||||
setSelected(nextActive ? [nextActive] : []);
|
||||
navigateTab(activeTab);
|
||||
}
|
||||
});
|
||||
},
|
||||
[cstDelete, schema, activeTab, activeCst, navigateTab]
|
||||
[activeTab, navigateTab]
|
||||
);
|
||||
|
||||
const onOpenCst = useCallback(
|
||||
|
@ -364,132 +152,15 @@ function RSTabs() {
|
|||
});
|
||||
}, [schema, destroyItem, router]);
|
||||
|
||||
const onClaimSchema = useCallback(() => {
|
||||
if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
|
||||
return;
|
||||
}
|
||||
claim(() => toast.success('Вы стали владельцем схемы'));
|
||||
}, [claim]);
|
||||
|
||||
const onShareSchema = useCallback(() => {
|
||||
const url = window.location.href + '&share';
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => toast.success(`Ссылка скопирована: ${url}`))
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const onShowTemplates = useCallback((selectedID?: number) => {
|
||||
setInsertCstID(selectedID);
|
||||
setShowTemplates(true);
|
||||
}, []);
|
||||
|
||||
const onDownloadSchema = useCallback(() => {
|
||||
if (isModified) {
|
||||
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const fileName = (schema?.alias ?? 'Schema') + EXTEOR_TRS_FILE;
|
||||
download((data: Blob) => {
|
||||
try {
|
||||
fileDownload(data, fileName);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}, [schema?.alias, download, isModified]);
|
||||
|
||||
const promptClone = useCallback(() => {
|
||||
if (isModified) {
|
||||
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setShowClone(true);
|
||||
}, [isModified]);
|
||||
|
||||
const handleToggleSubscribe = useCallback(() => {
|
||||
if (isSubscribed) {
|
||||
unsubscribe(() => toast.success('Отслеживание отключено'));
|
||||
} else {
|
||||
subscribe(() => toast.success('Отслеживание включено'));
|
||||
}
|
||||
}, [isSubscribed, subscribe, unsubscribe]);
|
||||
|
||||
const promptShowEditTerm = useCallback(() => {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
if (isModified) {
|
||||
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setShowEditTerm(true);
|
||||
}, [isModified, activeCst]);
|
||||
|
||||
const handleSaveWordforms = useCallback(
|
||||
(forms: TermForm[]) => {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
const data: ICstUpdateData = {
|
||||
id: activeCst.id,
|
||||
term_forms: forms
|
||||
};
|
||||
cstUpdate(data, () => toast.success('Изменения сохранены'));
|
||||
},
|
||||
[cstUpdate, activeCst]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null}
|
||||
{showClone ? <DlgCloneLibraryItem base={schema!} hideWindow={() => setShowClone(false)} /> : null}
|
||||
{showCreateCst ? (
|
||||
<DlgCreateCst
|
||||
hideWindow={() => setShowCreateCst(false)}
|
||||
onCreate={handleCreateCst}
|
||||
schema={schema!}
|
||||
initial={createInitialData}
|
||||
/>
|
||||
) : null}
|
||||
{showRenameCst ? (
|
||||
<DlgRenameCst
|
||||
hideWindow={() => setShowRenameCst(false)}
|
||||
onRename={handleRenameCst}
|
||||
initial={renameInitialData!}
|
||||
/>
|
||||
) : null}
|
||||
{showDeleteCst ? (
|
||||
<DlgDeleteCst
|
||||
schema={schema!}
|
||||
hideWindow={() => setShowDeleteCst(false)}
|
||||
onDelete={handleDeleteCst}
|
||||
selected={selected}
|
||||
/>
|
||||
) : null}
|
||||
{showEditTerm ? (
|
||||
<DlgEditWordForms
|
||||
hideWindow={() => setShowEditTerm(false)}
|
||||
onSave={handleSaveWordforms}
|
||||
target={activeCst!}
|
||||
/>
|
||||
) : null}
|
||||
{showTemplates ? (
|
||||
<DlgConstituentaTemplate
|
||||
schema={schema!}
|
||||
hideWindow={() => setShowTemplates(false)}
|
||||
insertAfter={insertCstID}
|
||||
onCreate={handleCreateCst}
|
||||
/>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
|
||||
{loading ? <Loader /> : null}
|
||||
{error ? <ProcessError error={error} /> : null}
|
||||
<RSEditState
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
activeCst={activeCst}
|
||||
isModified={isModified}
|
||||
onCreateCst={onCreateCst}
|
||||
onDeleteCst={onDeleteCst}
|
||||
>
|
||||
{schema && !loading ? (
|
||||
<Tabs
|
||||
selectedIndex={activeTab}
|
||||
|
@ -499,17 +170,8 @@ function RSTabs() {
|
|||
className='flex flex-col min-w-[45rem]'
|
||||
>
|
||||
<TabList className={clsx('mx-auto', 'flex', 'border-b-2 border-x-2 divide-x-2')}>
|
||||
<RSTabsMenu
|
||||
isMutable={isMutable}
|
||||
onTemplates={onShowTemplates}
|
||||
onDownload={onDownloadSchema}
|
||||
onDestroy={onDestroySchema}
|
||||
onClaim={onClaimSchema}
|
||||
onShare={onShareSchema}
|
||||
onReindex={onReindex}
|
||||
showCloneDialog={promptClone}
|
||||
showUploadDialog={() => setShowUpload(true)}
|
||||
/>
|
||||
<RSTabsMenu onDestroy={onDestroySchema} />
|
||||
|
||||
<TabLabel label='Карточка' title={`Название схемы: ${schema.title ?? ''}`} />
|
||||
<TabLabel
|
||||
label='Содержание'
|
||||
|
@ -522,106 +184,33 @@ function RSTabs() {
|
|||
<AnimateFade>
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '' : 'none' }}>
|
||||
<EditorRSForm
|
||||
isMutable={isMutable}
|
||||
isModified={isModified}
|
||||
isModified={isModified} // prettier: split lines
|
||||
setIsModified={setIsModified}
|
||||
onToggleSubscribe={handleToggleSubscribe}
|
||||
onDownload={onDownloadSchema}
|
||||
onDestroy={onDestroySchema}
|
||||
onClaim={onClaimSchema}
|
||||
onShare={onShareSchema}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '' : 'none' }}>
|
||||
<EditorRSList
|
||||
schema={schema}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
isMutable={isMutable}
|
||||
onMoveUp={handleMoveUp}
|
||||
onMoveDown={handleMoveDown}
|
||||
onOpenEdit={onOpenCst}
|
||||
onClone={handleCloneCst}
|
||||
onCreate={type => promptCreateCst(type, type !== undefined)}
|
||||
onDelete={() => setShowDeleteCst(true)}
|
||||
/>
|
||||
<EditorRSList selected={selected} setSelected={setSelected} onOpenEdit={onOpenCst} />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '' : 'none' }}>
|
||||
<EditorConstituenta
|
||||
schema={schema}
|
||||
isMutable={isMutable}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
activeCst={activeCst}
|
||||
onMoveUp={handleMoveUp}
|
||||
onMoveDown={handleMoveDown}
|
||||
onOpenEdit={onOpenCst}
|
||||
onClone={handleCloneCst}
|
||||
onCreate={type => promptCreateCst(type, false)}
|
||||
onDelete={() => setShowDeleteCst(true)}
|
||||
onRename={promptRenameCst}
|
||||
onEditTerm={promptShowEditTerm}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '' : 'none' }}>
|
||||
<EditorTermGraph
|
||||
schema={schema}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
isMutable={isMutable}
|
||||
onOpenEdit={onOpenCst}
|
||||
onCreate={(type, definition) => promptCreateCst(type, false, definition)}
|
||||
onDelete={() => setShowDeleteCst(true)}
|
||||
/>
|
||||
<EditorTermGraph selected={selected} setSelected={setSelected} onOpenEdit={onOpenCst} />
|
||||
</TabPanel>
|
||||
</AnimateFade>
|
||||
</Tabs>
|
||||
) : null}
|
||||
</>
|
||||
</RSEditState>
|
||||
);
|
||||
}
|
||||
|
||||
export default RSTabs;
|
||||
|
||||
// ====== Internals =========
|
||||
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
||||
if (axios.isAxiosError(error) && error.response && error.response.status === 404) {
|
||||
return (
|
||||
<div className='p-2 text-center'>
|
||||
<p>Схема с указанным идентификатором отсутствует на портале.</p>
|
||||
<TextURL text='Перейти в Библиотеку' href='/library' />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <InfoError error={error} />;
|
||||
}
|
||||
}
|
||||
|
||||
function getNextActiveOnDelete(
|
||||
activeID: number | undefined,
|
||||
items: IConstituenta[],
|
||||
deleted: number[]
|
||||
): number | undefined {
|
||||
if (items.length === deleted.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let activeIndex = items.findIndex(cst => cst.id === activeID);
|
||||
if (activeIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
while (activeIndex < items.length && deleted.find(id => id === items[activeIndex].id)) {
|
||||
++activeIndex;
|
||||
}
|
||||
if (activeIndex >= items.length) {
|
||||
activeIndex = items.length - 1;
|
||||
while (activeIndex >= 0 && deleted.find(id => id === items[activeIndex].id)) {
|
||||
--activeIndex;
|
||||
}
|
||||
}
|
||||
return items[activeIndex].id;
|
||||
}
|
||||
|
|
|
@ -26,31 +26,14 @@ import useDropdown from '@/hooks/useDropdown';
|
|||
import { UserAccessMode } from '@/models/miscellaneous';
|
||||
import { describeAccessMode, labelAccessMode } from '@/utils/labels';
|
||||
|
||||
import { useRSEdit } from './RSEditContext';
|
||||
|
||||
interface RSTabsMenuProps {
|
||||
isMutable: boolean;
|
||||
|
||||
showUploadDialog: () => void;
|
||||
showCloneDialog: () => void;
|
||||
|
||||
onDestroy: () => void;
|
||||
onClaim: () => void;
|
||||
onShare: () => void;
|
||||
onDownload: () => void;
|
||||
onReindex: () => void;
|
||||
onTemplates: () => void;
|
||||
}
|
||||
|
||||
function RSTabsMenu({
|
||||
isMutable,
|
||||
showUploadDialog,
|
||||
showCloneDialog,
|
||||
onDestroy,
|
||||
onShare,
|
||||
onDownload,
|
||||
onClaim,
|
||||
onReindex,
|
||||
onTemplates
|
||||
}: RSTabsMenuProps) {
|
||||
function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
||||
const controller = useRSEdit();
|
||||
const router = useConceptNavigation();
|
||||
const { user } = useAuth();
|
||||
const { isOwned, isClaimable } = useRSForm();
|
||||
|
@ -63,7 +46,7 @@ function RSTabsMenu({
|
|||
|
||||
function handleClaimOwner() {
|
||||
editMenu.hide();
|
||||
onClaim();
|
||||
controller.claim();
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
|
@ -73,32 +56,32 @@ function RSTabsMenu({
|
|||
|
||||
function handleDownload() {
|
||||
schemaMenu.hide();
|
||||
onDownload();
|
||||
controller.download();
|
||||
}
|
||||
|
||||
function handleUpload() {
|
||||
schemaMenu.hide();
|
||||
showUploadDialog();
|
||||
controller.promptUpload();
|
||||
}
|
||||
|
||||
function handleClone() {
|
||||
schemaMenu.hide();
|
||||
showCloneDialog();
|
||||
controller.promptClone();
|
||||
}
|
||||
|
||||
function handleShare() {
|
||||
schemaMenu.hide();
|
||||
onShare();
|
||||
controller.share();
|
||||
}
|
||||
|
||||
function handleReindex() {
|
||||
editMenu.hide();
|
||||
onReindex();
|
||||
controller.reindex();
|
||||
}
|
||||
|
||||
function handleTemplates() {
|
||||
editMenu.hide();
|
||||
onTemplates();
|
||||
controller.promptTemplate();
|
||||
}
|
||||
|
||||
function handleChangeMode(newMode: UserAccessMode) {
|
||||
|
@ -148,15 +131,15 @@ function RSTabsMenu({
|
|||
onClick={handleDownload}
|
||||
/>
|
||||
<DropdownButton
|
||||
disabled={!isMutable}
|
||||
disabled={!controller.isMutable}
|
||||
text='Загрузить из Экстеора'
|
||||
icon={<BiUpload size='1rem' className={isMutable ? 'clr-text-warning' : ''} />}
|
||||
icon={<BiUpload size='1rem' className={controller.isMutable ? 'clr-text-warning' : ''} />}
|
||||
onClick={handleUpload}
|
||||
/>
|
||||
<DropdownButton
|
||||
disabled={!isMutable}
|
||||
disabled={!controller.isMutable}
|
||||
text='Удалить схему'
|
||||
icon={<BiTrash size='1rem' className={isMutable ? 'clr-text-warning' : ''} />}
|
||||
icon={<BiTrash size='1rem' className={controller.isMutable ? 'clr-text-warning' : ''} />}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
<DropdownButton
|
||||
|
@ -176,22 +159,22 @@ function RSTabsMenu({
|
|||
hideTitle={editMenu.isOpen}
|
||||
className='h-full'
|
||||
style={{ outlineColor: 'transparent' }}
|
||||
icon={<FiEdit size='1.25rem' className={isMutable ? 'clr-text-success' : 'clr-text-warning'} />}
|
||||
icon={<FiEdit size='1.25rem' className={controller.isMutable ? 'clr-text-success' : 'clr-text-warning'} />}
|
||||
onClick={editMenu.toggle}
|
||||
/>
|
||||
<Dropdown isOpen={editMenu.isOpen}>
|
||||
<DropdownButton
|
||||
disabled={!isMutable}
|
||||
disabled={!controller.isMutable}
|
||||
text='Сброс имён'
|
||||
title='Присвоить порядковые имена и обновить выражения'
|
||||
icon={<BiAnalyse size='1rem' className={isMutable ? 'clr-text-primary' : ''} />}
|
||||
icon={<BiAnalyse size='1rem' className={controller.isMutable ? 'clr-text-primary' : ''} />}
|
||||
onClick={handleReindex}
|
||||
/>
|
||||
<DropdownButton
|
||||
disabled={!isMutable}
|
||||
disabled={!controller.isMutable}
|
||||
text='Банк выражений'
|
||||
title='Создать конституенту из шаблона'
|
||||
icon={<BiDiamond size='1rem' className={isMutable ? 'clr-text-success' : ''} />}
|
||||
icon={<BiDiamond size='1rem' className={controller.isMutable ? 'clr-text-success' : ''} />}
|
||||
onClick={handleTemplates}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
|
Loading…
Reference in New Issue
Block a user