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.CARD); const [activeID, setActiveID] = useState(undefined); const [showUpload, setShowUpload] = useState(false); const [showClone, setShowClone] = useState(false); const [syntaxTree, setSyntaxTree] = useState([]); const [expression, setExpression] = useState(''); const [showAST, setShowAST] = useState(false); const [afterDelete, setAfterDelete] = useState<((items: number[]) => void) | undefined>(undefined); const [toBeDeleted, setToBeDeleted] = useState([]); const [showDeleteCst, setShowDeleteCst] = useState(false); const [createInitialData, setCreateInitialData] = useState(); const [showCreateCst, setShowCreateCst] = useState(false); const [renameInitialData, setRenameInitialData] = useState(); 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 (
{ loading && } { error && } { schema && !loading && <> {showUpload && setShowUpload(false)} />} {showClone && setShowClone(false)} />} {showAST && setShowAST(false)} />} {showCreateCst && setShowCreateCst(false)} onCreate={handleCreateCst} initial={createInitialData} />} {showRenameCst && setShowRenameCst(false)} onRename={handleRenameCst} initial={renameInitialData} />} {showDeleteCst && setShowDeleteCst(false)} onDelete={handleDeleteCst} selected={toBeDeleted} />} setShowUpload(true)} /> Паспорт схемы Конституенты {`${schema.stats?.count_errors ?? 0} | ${schema.stats?.count_all ?? 0}`} Редактор Граф термов {schema.stats && } }
); } export default RSTabs;