ConceptPortal-public/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx
2023-09-04 19:12:27 +03:00

387 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fileDownload from 'js-file-download';
import { useCallback, useLayoutEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import { toast } from 'react-toastify';
import BackendError from '../../components/BackendError';
import { ConceptLoader } from '../../components/Common/ConceptLoader';
import ConceptTab from '../../components/Common/ConceptTab';
import { useLibrary } from '../../context/LibraryContext';
import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import useModificationPrompt from '../../hooks/useModificationPrompt';
import { prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants';
import { ICstCreateData, ICstRenameData, SyntaxTree } from '../../utils/models';
import { createAliasFor } from '../../utils/staticUI';
import DlgCloneRSForm from './DlgCloneRSForm';
import DlgCreateCst from './DlgCreateCst';
import DlgDeleteCst from './DlgDeleteCst';
import DlgRenameCst from './DlgRenameCst';
import DlgShowAST from './DlgShowAST';
import DlgUploadRSForm from './DlgUploadRSForm';
import EditorConstituenta from './EditorConstituenta';
import EditorItems from './EditorItems';
import EditorRSForm from './EditorRSForm';
import EditorTermGraph from './EditorTermGraph';
import RSFormStats from './elements/RSFormStats';
import RSTabsMenu from './RSTabsMenu';
export enum RSTabID {
CARD = 0,
CST_LIST = 1,
CST_EDIT = 2,
TERM_GRAPH = 3
}
function RSTabs() {
const navigate = useNavigate();
const search = useLocation().search;
const {
error, schema, loading, claim, download, isTracking,
cstCreate, cstDelete, cstRename, subscribe, unsubscribe
} = useRSForm();
const { destroySchema } = useLibrary();
const { setNoFooter } = useConceptTheme();
const { isModified, setIsModified } = useModificationPrompt();
const [activeTab, setActiveTab] = useState<RSTabID>(RSTabID.CARD);
const [activeID, setActiveID] = useState<number | undefined>(undefined);
const [showUpload, setShowUpload] = useState(false);
const [showClone, setShowClone] = useState(false);
const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]);
const [expression, setExpression] = useState('');
const [showAST, setShowAST] = useState(false);
const [afterDelete, setAfterDelete] = useState<((items: number[]) => void) | undefined>(undefined);
const [toBeDeleted, setToBeDeleted] = useState<number[]>([]);
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);
useLayoutEffect(() => {
if (schema) {
const oldTitle = document.title
document.title = schema.title
return () => {
document.title = oldTitle
}
}
}, [schema]);
useLayoutEffect(() => {
const activeTab = (Number(new URLSearchParams(search).get('tab')) ?? RSTabID.CARD) as RSTabID;
const cstQuery = new URLSearchParams(search).get('active');
setActiveTab(activeTab);
setNoFooter(activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST);
setActiveID(Number(cstQuery) ?? ((schema && schema?.items.length > 0) ? schema.items[0].id : undefined));
return () => setNoFooter(false);
}, [search, setActiveTab, setActiveID, schema, setNoFooter]);
function onSelectTab(index: number) {
navigateTo(index, activeID);
}
const navigateTo = useCallback(
(tab: RSTabID, activeID?: number) => {
if (!schema) {
return;
}
if (activeID) {
navigate(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`, {
replace: tab === activeTab && tab !== RSTabID.CST_EDIT
});
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
activeID = schema.items[0].id;
navigate(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`, { replace: true });
} else {
navigate(`/rsforms/${schema.id}?tab=${tab}`);
}
}, [navigate, schema, activeTab]);
const handleCreateCst = useCallback(
(data: ICstCreateData) => {
if (!schema?.items) {
return;
}
data.alias = createAliasFor(data.cst_type, schema);
cstCreate(data, newCst => {
toast.success(`Конституента добавлена: ${newCst.alias}`);
navigateTo(activeTab, newCst.id);
if (activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST) {
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'nearest'
});
}
}, TIMEOUT_UI_REFRESH);
}
});
}, [schema, cstCreate, navigateTo, activeTab]);
const promptCreateCst = useCallback(
(initialData: ICstCreateData, skipDialog?: boolean) => {
if (skipDialog) {
handleCreateCst(initialData);
} else {
setCreateInitialData(initialData);
setShowCreateCst(true);
}
}, [handleCreateCst]);
const handleRenameCst = useCallback(
(data: ICstRenameData) => {
cstRename(data, () => toast.success(`Переименование: ${renameInitialData!.alias} -> ${data.alias}`));
}, [cstRename, renameInitialData]);
const promptRenameCst = useCallback(
(initialData: ICstRenameData) => {
setRenameInitialData(initialData);
setShowRenameCst(true);
}, []);
const handleDeleteCst = useCallback(
(deleted: number[]) => {
if (!schema) {
return;
}
const data = {
items: deleted.map(id => {
return { id: id };
})
};
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}`);
if (deleted.length === schema.items.length) {
navigateTo(RSTabID.CST_LIST);
}
if (activeIndex) {
while (activeIndex < schema.items.length && deleted.find(id => id === schema.items[activeIndex].id)) {
++activeIndex;
}
if (activeIndex >= schema.items.length) {
activeIndex = schema.items.length - 1;
while (activeIndex >= 0 && deleted.find(id => id === schema.items[activeIndex].id)) {
--activeIndex;
}
}
navigateTo(activeTab, schema.items[activeIndex].id);
}
if (afterDelete) afterDelete(deleted);
});
}, [afterDelete, cstDelete, schema, activeID, activeTab, navigateTo]);
const promptDeleteCst = useCallback(
(selected: number[], callback?: (items: number[]) => void) => {
setAfterDelete(() => (
(items: number[]) => {
if (callback) callback(items);
}));
setToBeDeleted(selected);
setShowDeleteCst(true)
}, []);
const onShowAST = useCallback(
(expression: string, ast: SyntaxTree) => {
setSyntaxTree(ast);
setExpression(expression);
setShowAST(true);
}, []);
const onOpenCst = useCallback(
(cstID: number) => {
navigateTo(RSTabID.CST_EDIT, cstID)
}, [navigateTo]);
const onDestroySchema = useCallback(
() => {
if (!schema || !window.confirm('Вы уверены, что хотите удалить данную схему?')) {
return;
}
destroySchema(schema.id, () => {
toast.success('Схема удалена');
navigate('/library');
});
}, [schema, destroySchema, navigate]);
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 onDownloadSchema = useCallback(
() => {
if (isModified) {
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
return;
}
}
const fileName = (schema?.alias ?? 'Schema') + '.trs';
download(
(data) => {
try {
fileDownload(data, fileName);
} catch (error) {
console.error(error);
}
});
}, [schema?.alias, download, isModified]);
const handleShowClone = useCallback(
() => {
if (isModified) {
if (!window.confirm('Присутствуют несохраненные изменения. Продолжить без их учета?')) {
return;
}
}
setShowClone(true);
}, [isModified]);
const handleToggleSubscribe = useCallback(
() => {
if (isTracking) {
unsubscribe(
() => {
toast.success('Отслеживание отключено');
});
} else {
subscribe(
() => {
toast.success('Отслеживание включено');
});
}
}, [isTracking, subscribe, unsubscribe]);
return (
<div className='w-full'>
{ loading && <ConceptLoader /> }
{ error && <BackendError error={error} />}
{ schema && !loading && <>
{showUpload &&
<DlgUploadRSForm
hideWindow={() => setShowUpload(false)}
/>}
{showClone &&
<DlgCloneRSForm
hideWindow={() => setShowClone(false)}
/>}
{showAST &&
<DlgShowAST
expression={expression}
syntaxTree={syntaxTree}
hideWindow={() => setShowAST(false)}
/>}
{showCreateCst &&
<DlgCreateCst
hideWindow={() => setShowCreateCst(false)}
onCreate={handleCreateCst}
initial={createInitialData}
/>}
{showRenameCst &&
<DlgRenameCst
hideWindow={() => setShowRenameCst(false)}
onRename={handleRenameCst}
initial={renameInitialData}
/>}
{showDeleteCst &&
<DlgDeleteCst
hideWindow={() => setShowDeleteCst(false)}
onDelete={handleDeleteCst}
selected={toBeDeleted}
/>}
<Tabs
selectedIndex={activeTab}
onSelect={onSelectTab}
defaultFocus={true}
selectedTabClassName='font-semibold clr-selected'
>
<TabList className='flex items-start pl-2 border-b border-r-2 select-none justify-stretch w-fit clr-controls h-[1.9rem]'>
<RSTabsMenu
onDownload={onDownloadSchema}
onDestroy={onDestroySchema}
onClaim={onClaimSchema}
onShare={onShareSchema}
onToggleSubscribe={handleToggleSubscribe}
showCloneDialog={handleShowClone}
showUploadDialog={() => setShowUpload(true)}
/>
<ConceptTab className='border-r-2 min-w-[7.8rem]'>Паспорт схемы</ConceptTab>
<ConceptTab className='border-r-2 min-w-[10rem] flex justify-between gap-2'>
<span>Конституенты</span>
<span>{`${schema.stats?.count_errors ?? 0} | ${schema.stats?.count_all ?? 0}`}</span>
</ConceptTab>
<ConceptTab className='border-r-2 min-w-[5.2rem]'>Редактор</ConceptTab>
<ConceptTab className='min-w-[6.5rem]'>Граф термов</ConceptTab>
</TabList>
<TabPanel className='flex w-full gap-4'>
<EditorRSForm
isModified={isModified}
setIsModified={setIsModified}
onDownload={onDownloadSchema}
onDestroy={onDestroySchema}
onClaim={onClaimSchema}
onShare={onShareSchema}
/>
{schema.stats && <RSFormStats stats={schema.stats}/>}
</TabPanel>
<TabPanel className='w-full'>
<EditorItems
onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst}
/>
</TabPanel>
<TabPanel>
<EditorConstituenta
isModified={isModified}
setIsModified={setIsModified}
activeID={activeID}
onOpenEdit={onOpenCst}
onShowAST={onShowAST}
onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst}
onRenameCst={promptRenameCst}
/>
</TabPanel>
<TabPanel>
<EditorTermGraph
onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst}
/>
</TabPanel>
</Tabs>
</>}
</div>);
}
export default RSTabs;