ConceptPortal-public/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx

473 lines
16 KiB
TypeScript
Raw Normal View History

2023-09-04 22:17:04 +03:00
import axios from 'axios';
2023-08-25 22:51:20 +03:00
import fileDownload from 'js-file-download';
2023-09-11 20:31:54 +03:00
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
2023-09-05 00:23:53 +03:00
import { useLocation } from 'react-router-dom';
2023-07-25 20:27:29 +03:00
import { TabList, TabPanel, Tabs } from 'react-tabs';
2023-07-29 15:37:49 +03:00
import { toast } from 'react-toastify';
2023-07-25 20:27:29 +03:00
2023-09-04 22:17:04 +03:00
import BackendError, { ErrorInfo } from '../../components/BackendError';
import { ConceptLoader } from '../../components/common/ConceptLoader';
import ConceptTab from '../../components/common/ConceptTab';
import TextURL from '../../components/common/TextURL';
import { useLibrary } from '../../context/LibraryContext';
2023-09-05 00:23:53 +03:00
import { useConceptNavigation } from '../../context/NagivationContext';
2023-07-25 20:27:29 +03:00
import { useRSForm } from '../../context/RSFormContext';
2023-08-27 15:39:49 +03:00
import { useConceptTheme } from '../../context/ThemeContext';
2023-11-01 13:47:49 +03:00
import DlgCloneRSForm from '../../dialogs/DlgCloneRSForm';
2023-11-06 02:20:16 +03:00
import DlgConstituentaTemplate from '../../dialogs/DlgConstituentaTemplate';
2023-11-01 13:47:49 +03:00
import DlgCreateCst from '../../dialogs/DlgCreateCst';
import DlgDeleteCst from '../../dialogs/DlgDeleteCst';
import DlgEditWordForms from '../../dialogs/DlgEditWordForms';
import DlgRenameCst from '../../dialogs/DlgRenameCst';
import DlgShowAST from '../../dialogs/DlgShowAST';
import DlgUploadRSForm from '../../dialogs/DlgUploadRSForm';
2023-08-31 17:25:42 +03:00
import useModificationPrompt from '../../hooks/useModificationPrompt';
2023-09-25 14:17:52 +03:00
import { ICstCreateData, ICstRenameData, ICstUpdateData, TermForm } from '../../models/rsform';
2023-09-11 20:31:54 +03:00
import { SyntaxTree } from '../../models/rslang';
import { EXTEOR_TRS_FILE, prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants';
2023-10-08 18:15:59 +03:00
import { createAliasFor } from '../../utils/misc';
2023-07-28 00:03:37 +03:00
import EditorConstituenta from './EditorConstituenta';
import EditorItems from './EditorItems';
import EditorRSForm from './EditorRSForm';
2023-07-29 21:23:18 +03:00
import EditorTermGraph from './EditorTermGraph';
2023-10-16 01:22:08 +03:00
import RSTabsMenu from './elements/RSTabsMenu';
2023-07-15 17:46:19 +03:00
2023-08-27 15:39:49 +03:00
export enum RSTabID {
2023-07-15 17:46:19 +03:00
CARD = 0,
CST_LIST = 1,
2023-07-29 21:23:18 +03:00
CST_EDIT = 2,
TERM_GRAPH = 3
2023-07-15 17:46:19 +03:00
}
2023-09-04 22:17:04 +03:00
function ProcessError({error}: {error: ErrorInfo}): React.ReactElement {
if (axios.isAxiosError(error) && error.response && error.response.status === 404) {
return (
<div className='flex flex-col items-center justify-center w-full p-2'>
<p>Схема с указанным идентификатором отсутствует на портале.</p>
<TextURL text='Перейти в Библиотеку' href='/library'/>
</div>
);
} else {
return ( <BackendError error={error} />);
}
}
2023-07-27 22:04:25 +03:00
function RSTabs() {
2023-09-05 00:23:53 +03:00
const { navigateTo } = useConceptNavigation();
const search = useLocation().search;
const {
2023-08-26 17:26:49 +03:00
error, schema, loading, claim, download, isTracking,
2023-09-25 14:17:52 +03:00
cstCreate, cstDelete, cstRename, subscribe, unsubscribe, cstUpdate
} = useRSForm();
2023-11-01 13:41:32 +03:00
const { destroyItem } = useLibrary();
2023-10-14 17:14:40 +03:00
const { setNoFooter, noNavigation } = useConceptTheme();
2023-07-31 23:47:18 +03:00
2023-08-31 17:25:42 +03:00
const { isModified, setIsModified } = useModificationPrompt();
2023-11-06 02:20:16 +03:00
const [activeTab, setActiveTab] = useState(RSTabID.CARD);
const [activeID, setActiveID] = useState<number | undefined>(undefined);
2023-09-11 20:31:54 +03:00
const activeCst = useMemo(
() => schema?.items?.find(cst => cst.id === activeID)
, [schema?.items, activeID]);
2023-07-31 23:47:18 +03:00
2023-07-29 03:31:21 +03:00
const [showUpload, setShowUpload] = useState(false);
const [showClone, setShowClone] = useState(false);
2023-07-29 03:31:21 +03:00
const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]);
2023-08-01 21:55:18 +03:00
const [expression, setExpression] = useState('');
2023-07-29 03:31:21 +03:00
const [showAST, setShowAST] = useState(false);
2023-07-29 15:37:49 +03:00
const [afterDelete, setAfterDelete] = useState<((items: number[]) => void) | undefined>(undefined);
const [toBeDeleted, setToBeDeleted] = useState<number[]>([]);
const [showDeleteCst, setShowDeleteCst] = useState(false);
const [createInitialData, setCreateInitialData] = useState<ICstCreateData>();
2023-07-29 15:37:49 +03:00
const [showCreateCst, setShowCreateCst] = useState(false);
2023-08-23 01:36:17 +03:00
const [renameInitialData, setRenameInitialData] = useState<ICstRenameData>();
const [showRenameCst, setShowRenameCst] = useState(false);
2023-07-29 15:37:49 +03:00
2023-09-11 20:31:54 +03:00
const [showEditTerm, setShowEditTerm] = useState(false);
const [showTemplates, setShowTemplates] = useState(false);
2023-10-14 17:14:40 +03:00
const panelHeight = useMemo(
() => {
return !noNavigation ?
'calc(100vh - 4.8rem - 4px)'
: 'calc(100vh - 2rem - 4px)';
}, [noNavigation]);
2023-07-31 23:47:18 +03:00
useLayoutEffect(() => {
if (schema) {
const oldTitle = document.title
document.title = schema.title
return () => {
document.title = oldTitle
}
}
}, [schema]);
useLayoutEffect(() => {
2023-08-27 15:39:49 +03:00
const activeTab = (Number(new URLSearchParams(search).get('tab')) ?? RSTabID.CARD) as RSTabID;
const cstQuery = new URLSearchParams(search).get('active');
setActiveTab(activeTab);
2023-08-27 15:39:49 +03:00
setNoFooter(activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST);
2023-08-23 18:11:42 +03:00
setActiveID(Number(cstQuery) ?? ((schema && schema?.items.length > 0) ? schema.items[0].id : undefined));
2023-08-27 15:39:49 +03:00
return () => setNoFooter(false);
}, [search, setActiveTab, setActiveID, schema, setNoFooter]);
2023-07-31 23:47:18 +03:00
function onSelectTab(index: number) {
2023-09-05 00:23:53 +03:00
navigateTab(index, activeID);
2023-07-31 23:47:18 +03:00
}
2023-09-05 00:23:53 +03:00
const navigateTab = useCallback(
2023-08-27 15:39:49 +03:00
(tab: RSTabID, activeID?: number) => {
if (!schema) {
return;
}
if (activeID) {
2023-09-05 00:23:53 +03:00
navigateTo(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`, {
2023-08-27 15:39:49 +03:00
replace: tab === activeTab && tab !== RSTabID.CST_EDIT
});
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
activeID = schema.items[0].id;
2023-09-05 00:23:53 +03:00
navigateTo(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`, { replace: true });
} else {
2023-09-05 00:23:53 +03:00
navigateTo(`/rsforms/${schema.id}?tab=${tab}`);
}
2023-09-05 00:23:53 +03:00
}, [navigateTo, schema, activeTab]);
const handleCreateCst = useCallback(
(data: ICstCreateData) => {
2023-07-29 15:37:49 +03:00
if (!schema?.items) {
return;
}
2023-10-08 18:15:59 +03:00
data.alias = data.alias || createAliasFor(data.cst_type, schema);
2023-07-29 15:37:49 +03:00
cstCreate(data, newCst => {
toast.success(`Конституента добавлена: ${newCst.alias}`);
2023-09-05 00:23:53 +03:00
navigateTab(activeTab, newCst.id);
2023-08-27 15:39:49 +03:00
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);
2023-07-29 15:37:49 +03:00
}
});
2023-09-05 00:23:53 +03:00
}, [schema, cstCreate, navigateTab, activeTab]);
const promptCreateCst = useCallback(
(initialData: ICstCreateData, skipDialog?: boolean) => {
if (skipDialog) {
handleCreateCst(initialData);
} else {
setCreateInitialData(initialData);
setShowCreateCst(true);
}
}, [handleCreateCst]);
2023-08-23 01:36:17 +03:00
const handleRenameCst = useCallback(
(data: ICstRenameData) => {
2023-08-23 18:11:42 +03:00
cstRename(data, () => toast.success(`Переименование: ${renameInitialData!.alias} -> ${data.alias}`));
2023-08-23 01:36:17 +03:00
}, [cstRename, renameInitialData]);
const promptRenameCst = useCallback(
(initialData: ICstRenameData) => {
setRenameInitialData(initialData);
setShowRenameCst(true);
}, []);
const handleDeleteCst = useCallback(
(deleted: number[]) => {
if (!schema) {
return;
}
const data = {
items: deleted
};
let activeIndex = schema.items.findIndex(cst => cst.id === activeID);
cstDelete(data, () => {
const deletedNames = deleted.map(id => schema.items.find(cst => cst.id === id)?.alias).join(', ');
toast.success(`Конституенты удалены: ${deletedNames}`);
2023-11-23 19:34:37 +03:00
if (deleted.length === schema.items.length) {
2023-09-05 00:23:53 +03:00
navigateTab(RSTabID.CST_LIST);
2023-11-23 19:34:37 +03:00
} else if (activeIndex === -1) {
navigateTab(activeTab);
} else {
while (activeIndex < schema.items.length && deleted.find(id => id === schema.items[activeIndex].id)) {
++activeIndex;
}
2023-08-28 10:50:07 +03:00
if (activeIndex >= schema.items.length) {
activeIndex = schema.items.length - 1;
while (activeIndex >= 0 && deleted.find(id => id === schema.items[activeIndex].id)) {
--activeIndex;
}
}
2023-09-05 00:23:53 +03:00
navigateTab(activeTab, schema.items[activeIndex].id);
2023-07-29 15:37:49 +03:00
}
2023-11-23 19:34:37 +03:00
if (afterDelete) afterDelete(deleted);
});
2023-09-05 00:23:53 +03:00
}, [afterDelete, cstDelete, schema, activeID, activeTab, navigateTab]);
const promptDeleteCst = useCallback(
(selected: number[], callback?: (items: number[]) => void) => {
setAfterDelete(() => (
(items: number[]) => {
if (callback) callback(items);
}));
setToBeDeleted(selected);
setShowDeleteCst(true)
}, []);
2023-07-29 03:31:21 +03:00
const onShowAST = useCallback(
2023-08-01 21:55:18 +03:00
(expression: string, ast: SyntaxTree) => {
2023-07-29 03:31:21 +03:00
setSyntaxTree(ast);
2023-08-01 21:55:18 +03:00
setExpression(expression);
2023-07-29 03:31:21 +03:00
setShowAST(true);
2023-07-29 15:37:49 +03:00
}, []);
2023-07-28 18:23:37 +03:00
const onOpenCst = useCallback(
(cstID: number) => {
2023-09-05 00:23:53 +03:00
navigateTab(RSTabID.CST_EDIT, cstID)
}, [navigateTab]);
const onDestroySchema = useCallback(
() => {
if (!schema || !window.confirm('Вы уверены, что хотите удалить данную схему?')) {
return;
}
2023-11-01 13:41:32 +03:00
destroyItem(schema.id, () => {
toast.success('Схема удалена');
2023-09-05 00:23:53 +03:00
navigateTo('/library');
});
2023-11-01 13:41:32 +03:00
}, [schema, destroyItem, navigateTo]);
2023-08-25 22:51:20 +03:00
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(
() => {
setShowTemplates(true);
}, []);
2023-08-25 22:51:20 +03:00
const onDownloadSchema = useCallback(
() => {
2023-08-31 17:25:42 +03:00
if (isModified) {
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
return;
}
}
const fileName = (schema?.alias ?? 'Schema') + EXTEOR_TRS_FILE;
2023-08-25 22:51:20 +03:00
download(
2023-09-11 20:31:54 +03:00
(data: Blob) => {
2023-08-25 22:51:20 +03:00
try {
fileDownload(data, fileName);
} catch (error) {
console.error(error);
}
});
2023-08-31 17:25:42 +03:00
}, [schema?.alias, download, isModified]);
2023-09-11 20:31:54 +03:00
const promptClone = useCallback(
2023-08-31 17:25:42 +03:00
() => {
if (isModified) {
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
return;
}
}
setShowClone(true);
}, [isModified]);
2023-08-26 17:26:49 +03:00
const handleToggleSubscribe = useCallback(
() => {
if (isTracking) {
2023-09-11 20:31:54 +03:00
unsubscribe(() => toast.success('Отслеживание отключено'));
2023-08-26 17:26:49 +03:00
} else {
2023-09-11 20:31:54 +03:00
subscribe(() => toast.success('Отслеживание включено'));
2023-08-26 17:26:49 +03:00
}
}, [isTracking, subscribe, unsubscribe]);
2023-08-25 22:51:20 +03:00
2023-09-11 20:31:54 +03:00
const promptShowEditTerm = useCallback(
() => {
if (!activeCst) {
return;
}
if (isModified) {
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
return;
}
}
setShowEditTerm(true);
}, [isModified, activeCst]);
2023-09-25 14:17:52 +03:00
const handleSaveWordforms = useCallback(
(forms: TermForm[]) => {
if (!activeID) {
return;
}
const data: ICstUpdateData = {
id: activeID,
term_forms: forms
};
cstUpdate(data, () => toast.success('Изменения сохранены'));
}, [cstUpdate, activeID]);
2023-07-15 17:46:19 +03:00
return (
2023-07-20 17:11:03 +03:00
<div className='w-full'>
{ loading && <ConceptLoader /> }
2023-09-04 22:17:04 +03:00
{ error && <ProcessError error={error} />}
{ schema && !loading && <>
{showUpload &&
<DlgUploadRSForm
hideWindow={() => setShowUpload(false)}
/>}
{showClone &&
<DlgCloneRSForm
hideWindow={() => setShowClone(false)}
/>}
2023-07-29 15:37:49 +03:00
{showAST &&
<DlgShowAST
2023-08-01 21:55:18 +03:00
expression={expression}
2023-07-29 15:37:49 +03:00
syntaxTree={syntaxTree}
hideWindow={() => setShowAST(false)}
2023-07-29 15:37:49 +03:00
/>}
{showCreateCst &&
<DlgCreateCst
hideWindow={() => setShowCreateCst(false)}
onCreate={handleCreateCst}
2023-10-08 15:24:41 +03:00
schema={schema}
initial={createInitialData}
/>}
2023-08-23 01:36:17 +03:00
{showRenameCst &&
<DlgRenameCst
hideWindow={() => setShowRenameCst(false)}
onRename={handleRenameCst}
initial={renameInitialData!}
2023-08-23 01:36:17 +03:00
/>}
{showDeleteCst &&
<DlgDeleteCst
hideWindow={() => setShowDeleteCst(false)}
onDelete={handleDeleteCst}
selected={toBeDeleted}
2023-07-29 15:37:49 +03:00
/>}
2023-09-11 20:31:54 +03:00
{showEditTerm &&
2023-09-29 15:33:32 +03:00
<DlgEditWordForms
2023-09-11 20:31:54 +03:00
hideWindow={() => setShowEditTerm(false)}
2023-09-25 14:17:52 +03:00
onSave={handleSaveWordforms}
2023-09-11 20:31:54 +03:00
target={activeCst!}
/>}
{showTemplates &&
2023-11-06 02:20:16 +03:00
<DlgConstituentaTemplate
schema={schema}
hideWindow={() => setShowTemplates(false)}
onCreate={handleCreateCst}
/>}
2023-07-25 20:27:29 +03:00
<Tabs
2023-07-29 15:37:49 +03:00
selectedIndex={activeTab}
2023-07-20 17:11:03 +03:00
onSelect={onSelectTab}
2023-11-06 02:20:16 +03:00
defaultFocus
2023-10-13 23:11:41 +03:00
selectedTabClassName='clr-selected'
2023-10-23 18:22:55 +03:00
className='flex flex-col items-center w-full'
2023-07-20 17:11:03 +03:00
>
2023-10-13 23:11:41 +03:00
<TabList className='flex items-start border-b-2 border-x-2 select-none justify-stretch w-fit clr-controls h-[1.9rem] small-caps font-semibold'>
2023-07-28 18:23:37 +03:00
<RSTabsMenu
2023-08-25 22:51:20 +03:00
onDownload={onDownloadSchema}
onDestroy={onDestroySchema}
2023-08-25 22:51:20 +03:00
onClaim={onClaimSchema}
onShare={onShareSchema}
2023-08-26 17:26:49 +03:00
onToggleSubscribe={handleToggleSubscribe}
2023-09-11 20:31:54 +03:00
showCloneDialog={promptClone}
2023-09-04 19:12:27 +03:00
showUploadDialog={() => setShowUpload(true)}
2023-07-28 18:23:37 +03:00
/>
2023-10-14 23:46:36 +03:00
<ConceptTab
2023-11-06 02:20:16 +03:00
className='border-x-2'
tooltip={`Название схемы: ${schema.title ?? ''}`}
2023-10-14 23:46:36 +03:00
>
2023-11-10 15:34:59 +03:00
Карточка
2023-10-14 23:46:36 +03:00
</ConceptTab>
<ConceptTab
2023-11-06 02:20:16 +03:00
className='border-r-2'
tooltip={`Всего конституент: ${schema.stats?.count_all ?? 0}\nКоличество ошибок: ${schema.stats?.count_errors ?? 0}`}
2023-10-14 23:46:36 +03:00
>
2023-11-10 15:34:59 +03:00
Содержание
2023-07-20 17:11:03 +03:00
</ConceptTab>
2023-11-06 02:20:16 +03:00
<ConceptTab className='border-r-2'>
2023-10-14 23:46:36 +03:00
Редактор
</ConceptTab>
2023-11-06 02:20:16 +03:00
<ConceptTab className=''>
2023-10-14 23:46:36 +03:00
Граф термов
</ConceptTab>
2023-07-20 17:11:03 +03:00
</TabList>
2023-07-15 17:46:19 +03:00
2023-10-14 17:14:40 +03:00
<div className='overflow-y-auto' style={{ maxHeight: panelHeight}}>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '': 'none' }}>
2023-10-14 17:14:40 +03:00
<EditorRSForm
isModified={isModified}
setIsModified={setIsModified}
onDownload={onDownloadSchema}
onDestroy={onDestroySchema}
onClaim={onClaimSchema}
onShare={onShareSchema}
/>
</TabPanel>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '': 'none' }}>
2023-10-14 17:14:40 +03:00
<EditorItems
onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst}
onTemplates={() => onShowTemplates()} // TODO: implement insertion point
2023-10-14 17:14:40 +03:00
/>
</TabPanel>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '': 'none' }}>
2023-10-14 17:14:40 +03:00
<EditorConstituenta
isModified={isModified}
setIsModified={setIsModified}
activeID={activeID}
activeCst={activeCst}
onOpenEdit={onOpenCst}
onShowAST={onShowAST}
onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst}
onRenameCst={promptRenameCst}
onEditTerm={promptShowEditTerm}
/>
</TabPanel>
<TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '': 'none' }}>
2023-10-14 17:14:40 +03:00
<EditorTermGraph
onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst}
/>
</TabPanel>
</div>
2023-07-29 15:37:49 +03:00
</Tabs>
</>}
2023-07-15 17:46:19 +03:00
</div>);
}
2023-07-27 22:04:25 +03:00
export default RSTabs;