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 useLocalStorage from '@/hooks/useLocalStorage';
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
import { CstType, IConstituenta, IRSForm } from '@/models/rsform';
|
import { IConstituenta } from '@/models/rsform';
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { useRSEdit } from '../RSEditContext';
|
||||||
import ViewConstituents from '../ViewConstituents';
|
import ViewConstituents from '../ViewConstituents';
|
||||||
import ConstituentaToolbar from './ConstituentaToolbar';
|
import ConstituentaToolbar from './ConstituentaToolbar';
|
||||||
import FormConstituenta from './FormConstituenta';
|
import FormConstituenta from './FormConstituenta';
|
||||||
|
@ -19,48 +20,20 @@ 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;
|
|
||||||
|
|
||||||
activeCst?: IConstituenta;
|
activeCst?: IConstituenta;
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
|
||||||
onMoveUp: () => void;
|
|
||||||
onMoveDown: () => void;
|
|
||||||
onOpenEdit: (cstID: number) => void;
|
onOpenEdit: (cstID: number) => void;
|
||||||
onClone: () => void;
|
|
||||||
onCreate: (type?: CstType) => void;
|
|
||||||
onRename: () => void;
|
|
||||||
onEditTerm: () => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorConstituenta({
|
function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }: EditorConstituentaProps) {
|
||||||
schema,
|
const controller = useRSEdit();
|
||||||
isMutable,
|
|
||||||
isModified,
|
|
||||||
setIsModified,
|
|
||||||
activeCst,
|
|
||||||
onMoveUp,
|
|
||||||
onMoveDown,
|
|
||||||
onOpenEdit,
|
|
||||||
onClone,
|
|
||||||
onCreate,
|
|
||||||
onRename,
|
|
||||||
onEditTerm,
|
|
||||||
onDelete
|
|
||||||
}: EditorConstituentaProps) {
|
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
|
|
||||||
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 || !controller.isMutable, [activeCst, controller.isMutable]);
|
||||||
|
|
||||||
function handleCreate() {
|
|
||||||
onCreate(activeCst?.cst_type);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
|
@ -92,7 +65,7 @@ function EditorConstituenta({
|
||||||
function processAltKey(code: string): boolean {
|
function processAltKey(code: string): boolean {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 'KeyV':
|
case 'KeyV':
|
||||||
onClone();
|
controller.cloneCst();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -103,13 +76,13 @@ function EditorConstituenta({
|
||||||
<ConstituentaToolbar
|
<ConstituentaToolbar
|
||||||
isMutable={!disabled}
|
isMutable={!disabled}
|
||||||
isModified={isModified}
|
isModified={isModified}
|
||||||
onMoveUp={onMoveUp}
|
onMoveUp={controller.moveUp}
|
||||||
onMoveDown={onMoveDown}
|
onMoveDown={controller.moveDown}
|
||||||
onSubmit={initiateSubmit}
|
onSubmit={initiateSubmit}
|
||||||
onReset={() => setToggleReset(prev => !prev)}
|
onReset={() => setToggleReset(prev => !prev)}
|
||||||
onDelete={onDelete}
|
onDelete={controller.deleteCst}
|
||||||
onClone={onClone}
|
onClone={controller.cloneCst}
|
||||||
onCreate={handleCreate}
|
onCreate={() => controller.createCst(activeCst?.cst_type, false)}
|
||||||
/>
|
/>
|
||||||
<div tabIndex={-1} className='flex max-w-[95rem]' onKeyDown={handleInput}>
|
<div tabIndex={-1} className='flex max-w-[95rem]' onKeyDown={handleInput}>
|
||||||
<FormConstituenta
|
<FormConstituenta
|
||||||
|
@ -121,13 +94,13 @@ function EditorConstituenta({
|
||||||
toggleReset={toggleReset}
|
toggleReset={toggleReset}
|
||||||
onToggleList={() => setShowList(prev => !prev)}
|
onToggleList={() => setShowList(prev => !prev)}
|
||||||
setIsModified={setIsModified}
|
setIsModified={setIsModified}
|
||||||
onEditTerm={onEditTerm}
|
onEditTerm={controller.editTermForms}
|
||||||
onRename={onRename}
|
onRename={controller.renameCst}
|
||||||
/>
|
/>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showList && windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD ? (
|
{showList && windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD ? (
|
||||||
<ViewConstituents
|
<ViewConstituents
|
||||||
schema={schema}
|
schema={controller.schema}
|
||||||
expression={activeCst?.definition_formal ?? ''}
|
expression={activeCst?.definition_formal ?? ''}
|
||||||
baseHeight={UNFOLDED_HEIGHT}
|
baseHeight={UNFOLDED_HEIGHT}
|
||||||
activeID={activeCst?.id}
|
activeID={activeCst?.id}
|
||||||
|
|
|
@ -7,32 +7,19 @@ import { useAuth } from '@/context/AuthContext';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { useRSEdit } from '../RSEditContext';
|
||||||
import FormRSForm from './FormRSForm';
|
import FormRSForm from './FormRSForm';
|
||||||
import RSFormStats from './RSFormStats';
|
import RSFormStats from './RSFormStats';
|
||||||
import RSFormToolbar from './RSFormToolbar';
|
import RSFormToolbar from './RSFormToolbar';
|
||||||
|
|
||||||
interface EditorRSFormProps {
|
interface EditorRSFormProps {
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
isMutable: boolean;
|
|
||||||
|
|
||||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
onDestroy: () => void;
|
onDestroy: () => void;
|
||||||
onClaim: () => void;
|
|
||||||
onShare: () => void;
|
|
||||||
onDownload: () => void;
|
|
||||||
onToggleSubscribe: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorRSForm({
|
function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProps) {
|
||||||
isModified,
|
const { isMutable } = useRSEdit();
|
||||||
isMutable,
|
|
||||||
onDestroy,
|
|
||||||
onClaim,
|
|
||||||
onShare,
|
|
||||||
setIsModified,
|
|
||||||
onDownload,
|
|
||||||
onToggleSubscribe
|
|
||||||
}: EditorRSFormProps) {
|
|
||||||
const { schema, isClaimable, isSubscribed, processing } = useRSForm();
|
const { schema, isClaimable, isSubscribed, processing } = useRSForm();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
@ -55,18 +42,13 @@ function EditorRSForm({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RSFormToolbar
|
<RSFormToolbar
|
||||||
isMutable={isMutable}
|
|
||||||
processing={processing}
|
processing={processing}
|
||||||
isSubscribed={isSubscribed}
|
subscribed={isSubscribed}
|
||||||
modified={isModified}
|
modified={isModified}
|
||||||
claimable={isClaimable}
|
claimable={isClaimable}
|
||||||
anonymous={!user}
|
anonymous={!user}
|
||||||
onSubmit={initiateSubmit}
|
onSubmit={initiateSubmit}
|
||||||
onShare={onShare}
|
|
||||||
onDownload={onDownload}
|
|
||||||
onClaim={onClaim}
|
|
||||||
onDestroy={onDestroy}
|
onDestroy={onDestroy}
|
||||||
onToggleSubscribe={onToggleSubscribe}
|
|
||||||
/>
|
/>
|
||||||
<div tabIndex={-1} className='flex flex-col sm:flex-row w-fit' onKeyDown={handleInput}>
|
<div tabIndex={-1} className='flex flex-col sm:flex-row w-fit' onKeyDown={handleInput}>
|
||||||
<FlexColumn className='px-4 pb-2'>
|
<FlexColumn className='px-4 pb-2'>
|
||||||
|
|
|
@ -10,37 +10,29 @@ import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
|
|
||||||
interface RSFormToolbarProps {
|
import { useRSEdit } from '../RSEditContext';
|
||||||
isMutable: boolean;
|
|
||||||
isSubscribed: boolean;
|
|
||||||
modified: boolean;
|
|
||||||
claimable: boolean;
|
|
||||||
anonymous: boolean;
|
|
||||||
processing: boolean;
|
|
||||||
|
|
||||||
|
interface RSFormToolbarProps {
|
||||||
|
modified: boolean;
|
||||||
|
subscribed: boolean;
|
||||||
|
anonymous: boolean;
|
||||||
|
claimable: boolean;
|
||||||
|
processing: boolean;
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
onShare: () => void;
|
|
||||||
onDownload: () => void;
|
|
||||||
onClaim: () => void;
|
|
||||||
onDestroy: () => void;
|
onDestroy: () => void;
|
||||||
onToggleSubscribe: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function RSFormToolbar({
|
function RSFormToolbar({
|
||||||
isMutable,
|
|
||||||
modified,
|
modified,
|
||||||
claimable,
|
|
||||||
anonymous,
|
anonymous,
|
||||||
isSubscribed,
|
subscribed,
|
||||||
onToggleSubscribe,
|
claimable,
|
||||||
processing,
|
processing,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onShare,
|
|
||||||
onDownload,
|
|
||||||
onClaim,
|
|
||||||
onDestroy
|
onDestroy
|
||||||
}: RSFormToolbarProps) {
|
}: RSFormToolbarProps) {
|
||||||
const canSave = useMemo(() => modified && isMutable, [modified, isMutable]);
|
const controller = useRSEdit();
|
||||||
|
const canSave = useMemo(() => modified && controller.isMutable, [modified, controller.isMutable]);
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'>
|
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
@ -52,37 +44,37 @@ function RSFormToolbar({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Поделиться схемой'
|
title='Поделиться схемой'
|
||||||
icon={<BiShareAlt size='1.25rem' className='clr-text-primary' />}
|
icon={<BiShareAlt size='1.25rem' className='clr-text-primary' />}
|
||||||
onClick={onShare}
|
onClick={controller.share}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Скачать TRS файл'
|
title='Скачать TRS файл'
|
||||||
icon={<BiDownload size='1.25rem' className='clr-text-primary' />}
|
icon={<BiDownload size='1.25rem' className='clr-text-primary' />}
|
||||||
onClick={onDownload}
|
onClick={controller.download}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={`Отслеживание ${isSubscribed ? 'включено' : 'выключено'}`}
|
title={`Отслеживание ${subscribed ? 'включено' : 'выключено'}`}
|
||||||
disabled={anonymous || processing}
|
disabled={anonymous || processing}
|
||||||
icon={
|
icon={
|
||||||
isSubscribed ? (
|
subscribed ? (
|
||||||
<FiBell size='1.25rem' className='clr-text-primary' />
|
<FiBell size='1.25rem' className='clr-text-primary' />
|
||||||
) : (
|
) : (
|
||||||
<FiBellOff size='1.25rem' className='clr-text-controls' />
|
<FiBellOff size='1.25rem' className='clr-text-controls' />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
style={{ outlineColor: 'transparent' }}
|
style={{ outlineColor: 'transparent' }}
|
||||||
onClick={onToggleSubscribe}
|
onClick={controller.toggleSubscribe}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Стать владельцем'
|
title='Стать владельцем'
|
||||||
icon={<LuCrown size='1.25rem' className={!claimable || anonymous ? '' : 'clr-text-success'} />}
|
icon={<LuCrown size='1.25rem' className={!claimable || anonymous ? '' : 'clr-text-success'} />}
|
||||||
disabled={!claimable || anonymous || processing}
|
disabled={!claimable || anonymous || processing}
|
||||||
onClick={onClaim}
|
onClick={controller.claim}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Удалить схему'
|
title='Удалить схему'
|
||||||
disabled={!isMutable}
|
disabled={!controller.isMutable}
|
||||||
onClick={onDestroy}
|
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} />
|
<HelpButton topic={HelpTopic.RSFORM} offset={4} />
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
|
|
@ -4,57 +4,41 @@ 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 { CstType, IRSForm } from '@/models/rsform';
|
import { CstType } from '@/models/rsform';
|
||||||
|
|
||||||
|
import { useRSEdit } from '../RSEditContext';
|
||||||
import RSListToolbar from './RSListToolbar';
|
import RSListToolbar from './RSListToolbar';
|
||||||
import RSTable from './RSTable';
|
import RSTable from './RSTable';
|
||||||
|
|
||||||
interface EditorRSListProps {
|
interface EditorRSListProps {
|
||||||
schema?: IRSForm;
|
|
||||||
isMutable: boolean;
|
|
||||||
selected: number[];
|
selected: number[];
|
||||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||||
onMoveUp: () => void;
|
|
||||||
onMoveDown: () => void;
|
|
||||||
onOpenEdit: (cstID: number) => void;
|
onOpenEdit: (cstID: number) => void;
|
||||||
onClone: () => void;
|
|
||||||
onCreate: (type?: CstType) => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorRSList({
|
function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps) {
|
||||||
schema,
|
|
||||||
selected,
|
|
||||||
setSelected,
|
|
||||||
isMutable,
|
|
||||||
onMoveUp,
|
|
||||||
onMoveDown,
|
|
||||||
onOpenEdit,
|
|
||||||
onClone,
|
|
||||||
onCreate,
|
|
||||||
onDelete
|
|
||||||
}: EditorRSListProps) {
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
|
const controller = useRSEdit();
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!schema || selected.length === 0) {
|
if (!controller.schema || selected.length === 0) {
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
} else {
|
} else {
|
||||||
const newRowSelection: RowSelectionState = {};
|
const newRowSelection: RowSelectionState = {};
|
||||||
schema.items.forEach((cst, index) => {
|
controller.schema.items.forEach((cst, index) => {
|
||||||
newRowSelection[String(index)] = selected.includes(cst.id);
|
newRowSelection[String(index)] = selected.includes(cst.id);
|
||||||
});
|
});
|
||||||
setRowSelection(newRowSelection);
|
setRowSelection(newRowSelection);
|
||||||
}
|
}
|
||||||
}, [selected, schema]);
|
}, [selected, controller.schema]);
|
||||||
|
|
||||||
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
||||||
if (!schema) {
|
if (!controller.schema) {
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
} else {
|
} else {
|
||||||
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||||
const newSelection: number[] = [];
|
const newSelection: number[] = [];
|
||||||
schema?.items.forEach((cst, index) => {
|
controller.schema.items.forEach((cst, index) => {
|
||||||
if (newRowSelection[String(index)] === true) {
|
if (newRowSelection[String(index)] === true) {
|
||||||
newSelection.push(cst.id);
|
newSelection.push(cst.id);
|
||||||
}
|
}
|
||||||
|
@ -63,14 +47,13 @@ function EditorRSList({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement hotkeys for working with constituents table
|
|
||||||
function handleTableKey(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleTableKey(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (!isMutable) {
|
if (!controller.isMutable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === 'Delete' && selected.length > 0) {
|
if (event.key === 'Delete' && selected.length > 0) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onDelete();
|
controller.deleteCst();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!event.altKey || event.shiftKey) {
|
if (!event.altKey || event.shiftKey) {
|
||||||
|
@ -86,23 +69,23 @@ function EditorRSList({
|
||||||
if (selected.length > 0) {
|
if (selected.length > 0) {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 'ArrowUp': onMoveUp(); return true;
|
case 'ArrowUp': controller.moveUp(); return true;
|
||||||
case 'ArrowDown': onMoveDown(); return true;
|
case 'ArrowDown': controller.moveDown(); return true;
|
||||||
case 'KeyV': onClone(); return true;
|
case 'KeyV': controller.cloneCst(); return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 'Backquote': onCreate(); return true;
|
case 'Backquote': controller.createCst(undefined, false); return true;
|
||||||
|
|
||||||
case 'Digit1': onCreate(CstType.BASE); return true;
|
case 'Digit1': controller.createCst(CstType.BASE, true); return true;
|
||||||
case 'Digit2': onCreate(CstType.STRUCTURED); return true;
|
case 'Digit2': controller.createCst(CstType.STRUCTURED, true); return true;
|
||||||
case 'Digit3': onCreate(CstType.TERM); return true;
|
case 'Digit3': controller.createCst(CstType.TERM, true); return true;
|
||||||
case 'Digit4': onCreate(CstType.AXIOM); return true;
|
case 'Digit4': controller.createCst(CstType.AXIOM, true); return true;
|
||||||
case 'KeyQ': onCreate(CstType.FUNCTION); return true;
|
case 'KeyQ': controller.createCst(CstType.FUNCTION, true); return true;
|
||||||
case 'KeyW': onCreate(CstType.PREDICATE); return true;
|
case 'KeyW': controller.createCst(CstType.PREDICATE, true); return true;
|
||||||
case 'Digit5': onCreate(CstType.CONSTANT); return true;
|
case 'Digit5': controller.createCst(CstType.CONSTANT, true); return true;
|
||||||
case 'Digit6': onCreate(CstType.THEOREM); return true;
|
case 'Digit6': controller.createCst(CstType.THEOREM, true); return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -110,29 +93,21 @@ function EditorRSList({
|
||||||
return (
|
return (
|
||||||
<div tabIndex={-1} className='outline-none' onKeyDown={handleTableKey}>
|
<div tabIndex={-1} className='outline-none' onKeyDown={handleTableKey}>
|
||||||
<SelectedCounter
|
<SelectedCounter
|
||||||
totalCount={schema?.stats?.count_all ?? 0}
|
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||||
selectedCount={selected.length}
|
selectedCount={selected.length}
|
||||||
position='top-[0.3rem] left-2'
|
position='top-[0.3rem] left-2'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RSListToolbar
|
<RSListToolbar selectedCount={selected.length} />
|
||||||
selectedCount={selected.length}
|
|
||||||
isMutable={isMutable}
|
|
||||||
onMoveUp={onMoveUp}
|
|
||||||
onMoveDown={onMoveDown}
|
|
||||||
onClone={onClone}
|
|
||||||
onCreate={onCreate}
|
|
||||||
onDelete={onDelete}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='pt-[2.3rem] border-b' />
|
<div className='pt-[2.3rem] border-b' />
|
||||||
|
|
||||||
<RSTable
|
<RSTable
|
||||||
items={schema?.items}
|
items={controller.schema?.items}
|
||||||
selected={rowSelection}
|
selected={rowSelection}
|
||||||
setSelected={handleRowSelection}
|
setSelected={handleRowSelection}
|
||||||
onEdit={onOpenEdit}
|
onEdit={onOpenEdit}
|
||||||
onCreateNew={onCreate}
|
onCreateNew={() => controller.createCst(undefined, false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,26 +15,14 @@ import { getCstTypePrefix } from '@/models/rsformAPI';
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
import { getCstTypeShortcut, labelCstType } from '@/utils/labels';
|
import { getCstTypeShortcut, labelCstType } from '@/utils/labels';
|
||||||
|
|
||||||
interface RSListToolbarProps {
|
import { useRSEdit } from '../RSEditContext';
|
||||||
isMutable?: boolean;
|
|
||||||
selectedCount: number;
|
|
||||||
|
|
||||||
onMoveUp: () => void;
|
interface RSListToolbarProps {
|
||||||
onMoveDown: () => void;
|
selectedCount: number;
|
||||||
onDelete: () => void;
|
|
||||||
onClone: () => void;
|
|
||||||
onCreate: (type?: CstType) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function RSListToolbar({
|
function RSListToolbar({ selectedCount }: RSListToolbarProps) {
|
||||||
selectedCount,
|
const controller = useRSEdit();
|
||||||
isMutable,
|
|
||||||
onMoveUp,
|
|
||||||
onMoveDown,
|
|
||||||
onDelete,
|
|
||||||
onClone,
|
|
||||||
onCreate
|
|
||||||
}: RSListToolbarProps) {
|
|
||||||
const insertMenu = useDropdown();
|
const insertMenu = useDropdown();
|
||||||
const nothingSelected = useMemo(() => selectedCount === 0, [selectedCount]);
|
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'>
|
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex items-start'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Переместить вверх [Alt + вверх]'
|
title='Переместить вверх [Alt + вверх]'
|
||||||
icon={<BiUpvote size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-primary' : ''} />}
|
icon={
|
||||||
disabled={!isMutable || nothingSelected}
|
<BiUpvote size='1.25rem' className={controller.isMutable && !nothingSelected ? 'clr-text-primary' : ''} />
|
||||||
onClick={onMoveUp}
|
}
|
||||||
|
disabled={!controller.isMutable || nothingSelected}
|
||||||
|
onClick={controller.moveUp}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Переместить вниз [Alt + вниз]'
|
title='Переместить вниз [Alt + вниз]'
|
||||||
icon={<BiDownvote size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-primary' : ''} />}
|
icon={
|
||||||
disabled={!isMutable || nothingSelected}
|
<BiDownvote size='1.25rem' className={controller.isMutable && !nothingSelected ? 'clr-text-primary' : ''} />
|
||||||
onClick={onMoveDown}
|
}
|
||||||
|
disabled={!controller.isMutable || nothingSelected}
|
||||||
|
onClick={controller.moveDown}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Клонировать конституенту [Alt + V]'
|
title='Клонировать конституенту [Alt + V]'
|
||||||
icon={<BiDuplicate size='1.25rem' className={isMutable && selectedCount === 1 ? 'clr-text-success' : ''} />}
|
icon={
|
||||||
disabled={!isMutable || selectedCount !== 1}
|
<BiDuplicate
|
||||||
onClick={onClone}
|
size='1.25rem'
|
||||||
|
className={controller.isMutable && selectedCount === 1 ? 'clr-text-success' : ''}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
disabled={!controller.isMutable || selectedCount !== 1}
|
||||||
|
onClick={controller.cloneCst}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Добавить новую конституенту... [Alt + `]'
|
title='Добавить новую конституенту... [Alt + `]'
|
||||||
icon={<BiPlusCircle size='1.25rem' className={isMutable ? 'clr-text-success' : ''} />}
|
icon={<BiPlusCircle size='1.25rem' className={controller.isMutable ? 'clr-text-success' : ''} />}
|
||||||
disabled={!isMutable}
|
disabled={!controller.isMutable}
|
||||||
onClick={() => onCreate()}
|
onClick={() => controller.createCst(undefined, false)}
|
||||||
/>
|
/>
|
||||||
<div ref={insertMenu.ref}>
|
<div ref={insertMenu.ref}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Добавить пустую конституенту'
|
title='Добавить пустую конституенту'
|
||||||
hideTitle={insertMenu.isOpen}
|
hideTitle={insertMenu.isOpen}
|
||||||
icon={<BiDownArrowCircle size='1.25rem' className={isMutable ? 'clr-text-success' : ''} />}
|
icon={<BiDownArrowCircle size='1.25rem' className={controller.isMutable ? 'clr-text-success' : ''} />}
|
||||||
disabled={!isMutable}
|
disabled={!controller.isMutable}
|
||||||
onClick={insertMenu.toggle}
|
onClick={insertMenu.toggle}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={insertMenu.isOpen}>
|
<Dropdown isOpen={insertMenu.isOpen}>
|
||||||
|
@ -77,7 +74,7 @@ function RSListToolbar({
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
key={`${prefixes.csttype_list}${typeStr}`}
|
key={`${prefixes.csttype_list}${typeStr}`}
|
||||||
text={`${getCstTypePrefix(typeStr as CstType)}1 — ${labelCstType(typeStr as CstType)}`}
|
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)}
|
title={getCstTypeShortcut(typeStr as CstType)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -85,9 +82,9 @@ function RSListToolbar({
|
||||||
</div>
|
</div>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Удалить выбранные [Delete]'
|
title='Удалить выбранные [Delete]'
|
||||||
icon={<BiTrash size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-warning' : ''} />}
|
icon={<BiTrash size='1.25rem' className={controller.isMutable && !nothingSelected ? 'clr-text-warning' : ''} />}
|
||||||
disabled={!isMutable || nothingSelected}
|
disabled={!controller.isMutable || nothingSelected}
|
||||||
onClick={onDelete}
|
onClick={controller.deleteCst}
|
||||||
/>
|
/>
|
||||||
<HelpButton topic={HelpTopic.CSTLIST} offset={5} />
|
<HelpButton topic={HelpTopic.CSTLIST} offset={5} />
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
|
|
@ -12,10 +12,11 @@ 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, IRSForm } from '@/models/rsform';
|
import { CstType } 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';
|
||||||
|
|
||||||
|
import { useRSEdit } from '../RSEditContext';
|
||||||
import GraphSidebar from './GraphSidebar';
|
import GraphSidebar from './GraphSidebar';
|
||||||
import GraphToolbar from './GraphToolbar';
|
import GraphToolbar from './GraphToolbar';
|
||||||
import TermGraph from './TermGraph';
|
import TermGraph from './TermGraph';
|
||||||
|
@ -23,24 +24,13 @@ import useGraphFilter from './useGraphFilter';
|
||||||
import ViewHidden from './ViewHidden';
|
import ViewHidden from './ViewHidden';
|
||||||
|
|
||||||
interface EditorTermGraphProps {
|
interface EditorTermGraphProps {
|
||||||
isMutable: boolean;
|
|
||||||
selected: number[];
|
selected: number[];
|
||||||
schema?: IRSForm;
|
|
||||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||||
onOpenEdit: (cstID: number) => void;
|
onOpenEdit: (cstID: number) => void;
|
||||||
onCreate: (type: CstType, definition: string) => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorTermGraph({
|
function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphProps) {
|
||||||
schema,
|
const controller = useRSEdit();
|
||||||
selected,
|
|
||||||
setSelected,
|
|
||||||
isMutable,
|
|
||||||
onOpenEdit,
|
|
||||||
onCreate,
|
|
||||||
onDelete
|
|
||||||
}: EditorTermGraphProps) {
|
|
||||||
const { colors } = useConceptTheme();
|
const { colors } = useConceptTheme();
|
||||||
|
|
||||||
const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>('graph_filter', {
|
const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>('graph_filter', {
|
||||||
|
@ -59,7 +49,7 @@ function EditorTermGraph({
|
||||||
allowTheorem: true
|
allowTheorem: true
|
||||||
});
|
});
|
||||||
const [showParamsDialog, setShowParamsDialog] = useState(false);
|
const [showParamsDialog, setShowParamsDialog] = useState(false);
|
||||||
const filtered = useGraphFilter(schema, filterParams);
|
const filtered = useGraphFilter(controller.schema, filterParams);
|
||||||
|
|
||||||
const [hidden, setHidden] = useState<number[]>([]);
|
const [hidden, setHidden] = useState<number[]>([]);
|
||||||
|
|
||||||
|
@ -72,32 +62,32 @@ function EditorTermGraph({
|
||||||
|
|
||||||
const [hoverID, setHoverID] = useState<number | undefined>(undefined);
|
const [hoverID, setHoverID] = useState<number | undefined>(undefined);
|
||||||
const hoverCst = useMemo(() => {
|
const hoverCst = useMemo(() => {
|
||||||
return schema?.items.find(cst => cst.id === hoverID);
|
return controller.schema?.items.find(cst => cst.id === hoverID);
|
||||||
}, [schema?.items, hoverID]);
|
}, [controller.schema?.items, hoverID]);
|
||||||
|
|
||||||
const [toggleResetView, setToggleResetView] = useState(false);
|
const [toggleResetView, setToggleResetView] = useState(false);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!schema) {
|
if (!controller.schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newDismissed: number[] = [];
|
const newDismissed: number[] = [];
|
||||||
schema.items.forEach(cst => {
|
controller.schema.items.forEach(cst => {
|
||||||
if (!filtered.nodes.has(cst.id)) {
|
if (!filtered.nodes.has(cst.id)) {
|
||||||
newDismissed.push(cst.id);
|
newDismissed.push(cst.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setHidden(newDismissed);
|
setHidden(newDismissed);
|
||||||
setHoverID(undefined);
|
setHoverID(undefined);
|
||||||
}, [schema, filtered]);
|
}, [controller.schema, filtered]);
|
||||||
|
|
||||||
const nodes: GraphNode[] = useMemo(() => {
|
const nodes: GraphNode[] = useMemo(() => {
|
||||||
const result: GraphNode[] = [];
|
const result: GraphNode[] = [];
|
||||||
if (!schema) {
|
if (!controller.schema) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
filtered.nodes.forEach(node => {
|
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) {
|
if (cst) {
|
||||||
result.push({
|
result.push({
|
||||||
id: String(node.id),
|
id: String(node.id),
|
||||||
|
@ -107,7 +97,7 @@ function EditorTermGraph({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}, [schema, coloringScheme, filtered.nodes, filterParams.noText, colors]);
|
}, [controller.schema, coloringScheme, filtered.nodes, filterParams.noText, colors]);
|
||||||
|
|
||||||
const edges: GraphEdge[] = useMemo(() => {
|
const edges: GraphEdge[] = useMemo(() => {
|
||||||
const result: GraphEdge[] = [];
|
const result: GraphEdge[] = [];
|
||||||
|
@ -143,18 +133,18 @@ function EditorTermGraph({
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateCst() {
|
function handleCreateCst() {
|
||||||
if (!schema) {
|
if (!controller.schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const definition = selected.map(id => schema.items.find(cst => cst.id === id)!.alias).join(' ');
|
const definition = selected.map(id => controller.schema!.items.find(cst => cst.id === id)!.alias).join(' ');
|
||||||
onCreate(selected.length === 0 ? CstType.BASE : CstType.TERM, definition);
|
controller.createCst(selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteCst() {
|
function handleDeleteCst() {
|
||||||
if (!schema || selected.length === 0) {
|
if (!controller.schema || selected.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onDelete();
|
controller.deleteCst();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeLayout(newLayout: LayoutTypes) {
|
function handleChangeLayout(newLayout: LayoutTypes) {
|
||||||
|
@ -176,7 +166,7 @@ function EditorTermGraph({
|
||||||
|
|
||||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
// Hotkeys implementation
|
// Hotkeys implementation
|
||||||
if (!isMutable) {
|
if (!controller.isMutable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === 'Delete') {
|
if (event.key === 'Delete') {
|
||||||
|
@ -199,13 +189,13 @@ function EditorTermGraph({
|
||||||
|
|
||||||
<SelectedCounter
|
<SelectedCounter
|
||||||
hideZero
|
hideZero
|
||||||
totalCount={schema?.stats?.count_all ?? 0}
|
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||||
selectedCount={selected.length}
|
selectedCount={selected.length}
|
||||||
position='top-[0.3rem] left-0'
|
position='top-[0.3rem] left-0'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GraphToolbar
|
<GraphToolbar
|
||||||
isMutable={isMutable}
|
isMutable={controller.isMutable}
|
||||||
nothingSelected={nothingSelected}
|
nothingSelected={nothingSelected}
|
||||||
is3D={is3D}
|
is3D={is3D}
|
||||||
orbit={orbit}
|
orbit={orbit}
|
||||||
|
@ -243,7 +233,7 @@ function EditorTermGraph({
|
||||||
<ViewHidden
|
<ViewHidden
|
||||||
items={hidden}
|
items={hidden}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
schema={schema!}
|
schema={controller.schema}
|
||||||
coloringScheme={coloringScheme}
|
coloringScheme={coloringScheme}
|
||||||
toggleSelection={toggleDismissed}
|
toggleSelection={toggleDismissed}
|
||||||
onEdit={onOpenEdit}
|
onEdit={onOpenEdit}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { prefixes } from '@/utils/constants';
|
||||||
interface ViewHiddenProps {
|
interface ViewHiddenProps {
|
||||||
items: number[];
|
items: number[];
|
||||||
selected: number[];
|
selected: number[];
|
||||||
schema: IRSForm;
|
schema?: IRSForm;
|
||||||
coloringScheme: GraphColoringScheme;
|
coloringScheme: GraphColoringScheme;
|
||||||
|
|
||||||
toggleSelection: (cstID: number) => void;
|
toggleSelection: (cstID: number) => void;
|
||||||
|
@ -43,7 +43,7 @@ function ViewHidden({ items, selected, toggleSelection, schema, coloringScheme,
|
||||||
</p>
|
</p>
|
||||||
<div className='flex flex-wrap justify-center gap-2 py-2 overflow-y-auto' style={{ maxHeight: dismissedHeight }}>
|
<div className='flex flex-wrap justify-center gap-2 py-2 overflow-y-auto' style={{ maxHeight: dismissedHeight }}>
|
||||||
{items.map(cstID => {
|
{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 adjustedColoring = coloringScheme === 'none' ? 'status' : coloringScheme;
|
||||||
const id = `${prefixes.cst_hidden_list}${cst.alias}`;
|
const id = `${prefixes.cst_hidden_list}${cst.alias}`;
|
||||||
return (
|
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';
|
'use client';
|
||||||
|
|
||||||
import axios from 'axios';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
|
||||||
import fileDownload from 'js-file-download';
|
|
||||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import AnimateFade from '@/components/AnimateFade';
|
import AnimateFade from '@/components/AnimateFade';
|
||||||
import InfoError, { ErrorData } from '@/components/InfoError';
|
|
||||||
import Loader from '@/components/ui/Loader';
|
|
||||||
import TabLabel from '@/components/ui/TabLabel';
|
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 { useLibrary } from '@/context/LibraryContext';
|
||||||
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
|
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
import { useConceptTheme } from '@/context/ThemeContext';
|
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 useQueryStrings from '@/hooks/useQueryStrings';
|
||||||
import { UserAccessMode } from '@/models/miscellaneous';
|
import { IConstituenta, IConstituentaMeta } from '@/models/rsform';
|
||||||
import {
|
import { prefixes, TIMEOUT_UI_REFRESH } from '@/utils/constants';
|
||||||
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 EditorConstituenta from './EditorConstituenta';
|
import EditorConstituenta from './EditorConstituenta';
|
||||||
import EditorRSForm from './EditorRSForm';
|
import EditorRSForm from './EditorRSForm';
|
||||||
import EditorRSList from './EditorRSList';
|
import EditorRSList from './EditorRSList';
|
||||||
import EditorTermGraph from './EditorTermGraph';
|
import EditorTermGraph from './EditorTermGraph';
|
||||||
|
import { RSEditState } from './RSEditContext';
|
||||||
import RSTabsMenu from './RSTabsMenu';
|
import RSTabsMenu from './RSTabsMenu';
|
||||||
|
|
||||||
export enum RSTabID {
|
export enum RSTabID {
|
||||||
|
@ -59,41 +35,13 @@ function RSTabs() {
|
||||||
const activeTab = (Number(query.get('tab')) ?? RSTabID.CARD) as RSTabID;
|
const activeTab = (Number(query.get('tab')) ?? RSTabID.CARD) as RSTabID;
|
||||||
const cstQuery = query.get('active');
|
const cstQuery = query.get('active');
|
||||||
|
|
||||||
const {
|
const { schema, loading } = useRSForm();
|
||||||
error,
|
|
||||||
schema,
|
|
||||||
loading,
|
|
||||||
processing,
|
|
||||||
isOwned,
|
|
||||||
claim,
|
|
||||||
download,
|
|
||||||
isSubscribed,
|
|
||||||
cstCreate,
|
|
||||||
cstDelete,
|
|
||||||
cstRename,
|
|
||||||
subscribe,
|
|
||||||
unsubscribe,
|
|
||||||
cstUpdate,
|
|
||||||
cstMoveTo,
|
|
||||||
resetAliases
|
|
||||||
} = useRSForm();
|
|
||||||
const { destroyItem } = useLibrary();
|
const { destroyItem } = useLibrary();
|
||||||
const { setNoFooter } = useConceptTheme();
|
const { setNoFooter } = useConceptTheme();
|
||||||
const { user } = useAuth();
|
|
||||||
const { mode, setMode } = useAccessMode();
|
|
||||||
|
|
||||||
const [isModified, setIsModified] = useState(false);
|
const [isModified, setIsModified] = useState(false);
|
||||||
useBlockNavigation(isModified);
|
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 [selected, setSelected] = useState<number[]>([]);
|
||||||
const activeCst: IConstituenta | undefined = useMemo(() => {
|
const activeCst: IConstituenta | undefined = useMemo(() => {
|
||||||
if (!schema || selected.length === 0) {
|
if (!schema || selected.length === 0) {
|
||||||
|
@ -103,22 +51,6 @@ function RSTabs() {
|
||||||
}
|
}
|
||||||
}, [schema, selected]);
|
}, [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(() => {
|
useLayoutEffect(() => {
|
||||||
if (schema) {
|
if (schema) {
|
||||||
const oldTitle = document.title;
|
const oldTitle = document.title;
|
||||||
|
@ -145,20 +77,6 @@ function RSTabs() {
|
||||||
return () => setNoFooter(false);
|
return () => setNoFooter(false);
|
||||||
}, [activeTab, cstQuery, setSelected, schema, setNoFooter, setIsModified]);
|
}, [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(
|
const navigateTab = useCallback(
|
||||||
(tab: RSTabID, activeID?: number) => {
|
(tab: RSTabID, activeID?: number) => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
|
@ -184,15 +102,8 @@ function RSTabs() {
|
||||||
navigateTab(index, selected.length > 0 ? selected.at(-1) : undefined);
|
navigateTab(index, selected.length > 0 ? selected.at(-1) : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateCst = useCallback(
|
const onCreateCst = useCallback(
|
||||||
(data: ICstCreateData) => {
|
(newCst: IConstituentaMeta) => {
|
||||||
if (!schema?.items) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
data.alias = data.alias || generateAlias(data.cst_type, schema);
|
|
||||||
cstCreate(data, newCst => {
|
|
||||||
toast.success(`Конституента добавлена: ${newCst.alias}`);
|
|
||||||
setSelected([newCst.id]);
|
|
||||||
navigateTab(activeTab, newCst.id);
|
navigateTab(activeTab, newCst.id);
|
||||||
if (activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST) {
|
if (activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -206,144 +117,21 @@ function RSTabs() {
|
||||||
}
|
}
|
||||||
}, TIMEOUT_UI_REFRESH);
|
}, TIMEOUT_UI_REFRESH);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[schema, cstCreate, navigateTab, activeTab]
|
[activeTab, navigateTab]
|
||||||
);
|
);
|
||||||
|
|
||||||
const promptCreateCst = useCallback(
|
const onDeleteCst = useCallback(
|
||||||
(type: CstType | undefined, skipDialog: boolean, definition?: string) => {
|
(newActive?: number) => {
|
||||||
const data: ICstCreateData = {
|
if (!newActive) {
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[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);
|
navigateTab(RSTabID.CST_LIST);
|
||||||
} else if (activeTab === RSTabID.CST_EDIT) {
|
} else if (activeTab === RSTabID.CST_EDIT) {
|
||||||
navigateTab(activeTab, nextActive);
|
navigateTab(activeTab, newActive);
|
||||||
} else {
|
} else {
|
||||||
setSelected(nextActive ? [nextActive] : []);
|
|
||||||
navigateTab(activeTab);
|
navigateTab(activeTab);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[cstDelete, schema, activeTab, activeCst, navigateTab]
|
[activeTab, navigateTab]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onOpenCst = useCallback(
|
const onOpenCst = useCallback(
|
||||||
|
@ -364,132 +152,15 @@ function RSTabs() {
|
||||||
});
|
});
|
||||||
}, [schema, destroyItem, router]);
|
}, [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 (
|
return (
|
||||||
<>
|
<RSEditState
|
||||||
<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}
|
selected={selected}
|
||||||
/>
|
setSelected={setSelected}
|
||||||
) : null}
|
activeCst={activeCst}
|
||||||
{showEditTerm ? (
|
isModified={isModified}
|
||||||
<DlgEditWordForms
|
onCreateCst={onCreateCst}
|
||||||
hideWindow={() => setShowEditTerm(false)}
|
onDeleteCst={onDeleteCst}
|
||||||
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}
|
|
||||||
{schema && !loading ? (
|
{schema && !loading ? (
|
||||||
<Tabs
|
<Tabs
|
||||||
selectedIndex={activeTab}
|
selectedIndex={activeTab}
|
||||||
|
@ -499,17 +170,8 @@ function RSTabs() {
|
||||||
className='flex flex-col min-w-[45rem]'
|
className='flex flex-col min-w-[45rem]'
|
||||||
>
|
>
|
||||||
<TabList className={clsx('mx-auto', 'flex', 'border-b-2 border-x-2 divide-x-2')}>
|
<TabList className={clsx('mx-auto', 'flex', 'border-b-2 border-x-2 divide-x-2')}>
|
||||||
<RSTabsMenu
|
<RSTabsMenu onDestroy={onDestroySchema} />
|
||||||
isMutable={isMutable}
|
|
||||||
onTemplates={onShowTemplates}
|
|
||||||
onDownload={onDownloadSchema}
|
|
||||||
onDestroy={onDestroySchema}
|
|
||||||
onClaim={onClaimSchema}
|
|
||||||
onShare={onShareSchema}
|
|
||||||
onReindex={onReindex}
|
|
||||||
showCloneDialog={promptClone}
|
|
||||||
showUploadDialog={() => setShowUpload(true)}
|
|
||||||
/>
|
|
||||||
<TabLabel label='Карточка' title={`Название схемы: ${schema.title ?? ''}`} />
|
<TabLabel label='Карточка' title={`Название схемы: ${schema.title ?? ''}`} />
|
||||||
<TabLabel
|
<TabLabel
|
||||||
label='Содержание'
|
label='Содержание'
|
||||||
|
@ -522,106 +184,33 @@ function RSTabs() {
|
||||||
<AnimateFade>
|
<AnimateFade>
|
||||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '' : 'none' }}>
|
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '' : 'none' }}>
|
||||||
<EditorRSForm
|
<EditorRSForm
|
||||||
isMutable={isMutable}
|
isModified={isModified} // prettier: split lines
|
||||||
isModified={isModified}
|
|
||||||
setIsModified={setIsModified}
|
setIsModified={setIsModified}
|
||||||
onToggleSubscribe={handleToggleSubscribe}
|
|
||||||
onDownload={onDownloadSchema}
|
|
||||||
onDestroy={onDestroySchema}
|
onDestroy={onDestroySchema}
|
||||||
onClaim={onClaimSchema}
|
|
||||||
onShare={onShareSchema}
|
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '' : 'none' }}>
|
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '' : 'none' }}>
|
||||||
<EditorRSList
|
<EditorRSList selected={selected} setSelected={setSelected} onOpenEdit={onOpenCst} />
|
||||||
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)}
|
|
||||||
/>
|
|
||||||
</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}
|
|
||||||
isModified={isModified}
|
isModified={isModified}
|
||||||
setIsModified={setIsModified}
|
setIsModified={setIsModified}
|
||||||
activeCst={activeCst}
|
activeCst={activeCst}
|
||||||
onMoveUp={handleMoveUp}
|
|
||||||
onMoveDown={handleMoveDown}
|
|
||||||
onOpenEdit={onOpenCst}
|
onOpenEdit={onOpenCst}
|
||||||
onClone={handleCloneCst}
|
|
||||||
onCreate={type => promptCreateCst(type, false)}
|
|
||||||
onDelete={() => setShowDeleteCst(true)}
|
|
||||||
onRename={promptRenameCst}
|
|
||||||
onEditTerm={promptShowEditTerm}
|
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '' : 'none' }}>
|
<TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '' : 'none' }}>
|
||||||
<EditorTermGraph
|
<EditorTermGraph selected={selected} setSelected={setSelected} onOpenEdit={onOpenCst} />
|
||||||
schema={schema}
|
|
||||||
selected={selected}
|
|
||||||
setSelected={setSelected}
|
|
||||||
isMutable={isMutable}
|
|
||||||
onOpenEdit={onOpenCst}
|
|
||||||
onCreate={(type, definition) => promptCreateCst(type, false, definition)}
|
|
||||||
onDelete={() => setShowDeleteCst(true)}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</AnimateFade>
|
</AnimateFade>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</RSEditState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RSTabs;
|
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 { UserAccessMode } from '@/models/miscellaneous';
|
||||||
import { describeAccessMode, labelAccessMode } from '@/utils/labels';
|
import { describeAccessMode, labelAccessMode } from '@/utils/labels';
|
||||||
|
|
||||||
|
import { useRSEdit } from './RSEditContext';
|
||||||
|
|
||||||
interface RSTabsMenuProps {
|
interface RSTabsMenuProps {
|
||||||
isMutable: boolean;
|
|
||||||
|
|
||||||
showUploadDialog: () => void;
|
|
||||||
showCloneDialog: () => void;
|
|
||||||
|
|
||||||
onDestroy: () => void;
|
onDestroy: () => void;
|
||||||
onClaim: () => void;
|
|
||||||
onShare: () => void;
|
|
||||||
onDownload: () => void;
|
|
||||||
onReindex: () => void;
|
|
||||||
onTemplates: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function RSTabsMenu({
|
function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
||||||
isMutable,
|
const controller = useRSEdit();
|
||||||
showUploadDialog,
|
|
||||||
showCloneDialog,
|
|
||||||
onDestroy,
|
|
||||||
onShare,
|
|
||||||
onDownload,
|
|
||||||
onClaim,
|
|
||||||
onReindex,
|
|
||||||
onTemplates
|
|
||||||
}: RSTabsMenuProps) {
|
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { isOwned, isClaimable } = useRSForm();
|
const { isOwned, isClaimable } = useRSForm();
|
||||||
|
@ -63,7 +46,7 @@ function RSTabsMenu({
|
||||||
|
|
||||||
function handleClaimOwner() {
|
function handleClaimOwner() {
|
||||||
editMenu.hide();
|
editMenu.hide();
|
||||||
onClaim();
|
controller.claim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
|
@ -73,32 +56,32 @@ function RSTabsMenu({
|
||||||
|
|
||||||
function handleDownload() {
|
function handleDownload() {
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
onDownload();
|
controller.download();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpload() {
|
function handleUpload() {
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
showUploadDialog();
|
controller.promptUpload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClone() {
|
function handleClone() {
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
showCloneDialog();
|
controller.promptClone();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShare() {
|
function handleShare() {
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
onShare();
|
controller.share();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReindex() {
|
function handleReindex() {
|
||||||
editMenu.hide();
|
editMenu.hide();
|
||||||
onReindex();
|
controller.reindex();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTemplates() {
|
function handleTemplates() {
|
||||||
editMenu.hide();
|
editMenu.hide();
|
||||||
onTemplates();
|
controller.promptTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeMode(newMode: UserAccessMode) {
|
function handleChangeMode(newMode: UserAccessMode) {
|
||||||
|
@ -148,15 +131,15 @@ function RSTabsMenu({
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
disabled={!isMutable}
|
disabled={!controller.isMutable}
|
||||||
text='Загрузить из Экстеора'
|
text='Загрузить из Экстеора'
|
||||||
icon={<BiUpload size='1rem' className={isMutable ? 'clr-text-warning' : ''} />}
|
icon={<BiUpload size='1rem' className={controller.isMutable ? 'clr-text-warning' : ''} />}
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
disabled={!isMutable}
|
disabled={!controller.isMutable}
|
||||||
text='Удалить схему'
|
text='Удалить схему'
|
||||||
icon={<BiTrash size='1rem' className={isMutable ? 'clr-text-warning' : ''} />}
|
icon={<BiTrash size='1rem' className={controller.isMutable ? 'clr-text-warning' : ''} />}
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
|
@ -176,22 +159,22 @@ function RSTabsMenu({
|
||||||
hideTitle={editMenu.isOpen}
|
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={controller.isMutable ? 'clr-text-success' : 'clr-text-warning'} />}
|
||||||
onClick={editMenu.toggle}
|
onClick={editMenu.toggle}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={editMenu.isOpen}>
|
<Dropdown isOpen={editMenu.isOpen}>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
disabled={!isMutable}
|
disabled={!controller.isMutable}
|
||||||
text='Сброс имён'
|
text='Сброс имён'
|
||||||
title='Присвоить порядковые имена и обновить выражения'
|
title='Присвоить порядковые имена и обновить выражения'
|
||||||
icon={<BiAnalyse size='1rem' className={isMutable ? 'clr-text-primary' : ''} />}
|
icon={<BiAnalyse size='1rem' className={controller.isMutable ? 'clr-text-primary' : ''} />}
|
||||||
onClick={handleReindex}
|
onClick={handleReindex}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
disabled={!isMutable}
|
disabled={!controller.isMutable}
|
||||||
text='Банк выражений'
|
text='Банк выражений'
|
||||||
title='Создать конституенту из шаблона'
|
title='Создать конституенту из шаблона'
|
||||||
icon={<BiDiamond size='1rem' className={isMutable ? 'clr-text-success' : ''} />}
|
icon={<BiDiamond size='1rem' className={controller.isMutable ? 'clr-text-success' : ''} />}
|
||||||
onClick={handleTemplates}
|
onClick={handleTemplates}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user