'use client'; import axios from 'axios'; import clsx from 'clsx'; import { AnimatePresence } from 'framer-motion'; import fileDownload from 'js-file-download'; import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { TabList, TabPanel, Tabs } from 'react-tabs'; import { toast } from 'react-toastify'; import AnimateFadeIn from '@/components/AnimateFadeIn'; import InfoError, { ErrorData } from '@/components/InfoError'; import { Loader } from '@/components/ui/Loader'; import TabLabel from '@/components/ui/TabLabel'; import TextURL from '@/components/ui/TextURL'; import { useAccessMode } from '@/context/AccessModeContext'; import { useAuth } from '@/context/AuthContext'; import { useLibrary } from '@/context/LibraryContext'; import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext'; import { useRSForm } from '@/context/RSFormContext'; import { useConceptTheme } from '@/context/ThemeContext'; import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem'; import DlgConstituentaTemplate from '@/dialogs/DlgConstituentaTemplate'; import DlgCreateCst from '@/dialogs/DlgCreateCst'; import DlgDeleteCst from '@/dialogs/DlgDeleteCst'; import DlgEditWordForms from '@/dialogs/DlgEditWordForms'; import DlgRenameCst from '@/dialogs/DlgRenameCst'; import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm'; import useQueryStrings from '@/hooks/useQueryStrings'; import { UserAccessMode } from '@/models/miscellaneous'; import { IConstituenta, ICstCreateData, 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 EditorRSForm from './EditorRSForm'; import EditorRSList from './EditorRSList'; import EditorTermGraph from './EditorTermGraph'; import RSTabsMenu from './RSTabsMenu'; export enum RSTabID { CARD = 0, CST_LIST = 1, CST_EDIT = 2, TERM_GRAPH = 3 } function ProcessError({ error }: { error: ErrorData }): React.ReactElement { if (axios.isAxiosError(error) && error.response && error.response.status === 404) { return (

Схема с указанным идентификатором отсутствует на портале.

); } else { return ; } } function RSTabs() { const router = useConceptNavigation(); const query = useQueryStrings(); const activeTab = (Number(query.get('tab')) ?? RSTabID.CARD) as RSTabID; const cstQuery = query.get('active'); const { error, schema, loading, processing, isOwned, claim, download, isSubscribed, cstCreate, cstDelete, cstRename, subscribe, unsubscribe, cstUpdate, resetAliases } = useRSForm(); const { destroyItem } = useLibrary(); const { setNoFooter } = useConceptTheme(); const { user } = useAuth(); const { mode, setMode } = useAccessMode(); const [isModified, setIsModified] = useState(false); useBlockNavigation(isModified); const isMutable = useMemo(() => { return ( !loading && !processing && mode !== UserAccessMode.READER && ((isOwned || (mode === UserAccessMode.ADMIN && user?.is_staff)) ?? false) ); }, [user?.is_staff, mode, isOwned, loading, processing]); const [activeID, setActiveID] = useState(undefined); const activeCst = useMemo(() => schema?.items?.find(cst => cst.id === activeID), [schema?.items, activeID]); const [showUpload, setShowUpload] = useState(false); const [showClone, setShowClone] = 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); const [showEditTerm, setShowEditTerm] = useState(false); const [insertCstID, setInsertCstID] = useState(undefined); const [showTemplates, setShowTemplates] = useState(false); useLayoutEffect(() => { if (schema) { const oldTitle = document.title; document.title = schema.title; return () => { document.title = oldTitle; }; } }, [schema, schema?.title]); useLayoutEffect(() => { setNoFooter(activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST); setActiveID(Number(cstQuery) ?? (schema && schema?.items.length > 0 ? schema.items[0].id : undefined)); setIsModified(false); return () => setNoFooter(false); }, [activeTab, cstQuery, setActiveID, 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] ); function onSelectTab(index: number) { navigateTab(index, activeID); } const navigateTab = useCallback( (tab: RSTabID, activeID?: number) => { if (!schema) { return; } if (activeID) { if (tab === activeTab && tab !== RSTabID.CST_EDIT) { router.replace(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`); } else { router.push(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`); } } else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) { activeID = schema.items[0].id; router.replace(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`); } else { router.push(`/rsforms/${schema.id}?tab=${tab}`); } }, [router, schema, activeTab] ); const handleCreateCst = useCallback( (data: ICstCreateData) => { if (!schema?.items) { return; } data.alias = data.alias || generateAlias(data.cst_type, schema); cstCreate(data, newCst => { toast.success(`Конституента добавлена: ${newCst.alias}`); navigateTab(activeTab, newCst.id); if (activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST) { setTimeout(() => { const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); } }, TIMEOUT_UI_REFRESH); } }); }, [schema, cstCreate, navigateTab, activeTab] ); 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 onReindex = useCallback(() => resetAliases(() => toast.success('Имена конституент обновлены')), [resetAliases]); 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(activeID, schema.items, deleted); cstDelete(data, () => { toast.success(`Конституенты удалены: ${deletedNames}`); if (isEmpty) { navigateTab(RSTabID.CST_LIST); } else if (!nextActive) { navigateTab(activeTab); } else { navigateTab(activeTab, nextActive); } if (afterDelete) afterDelete(deleted); }); }, [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); }, []); const onOpenCst = useCallback( (cstID: number) => { navigateTab(RSTabID.CST_EDIT, cstID); }, [navigateTab] ); const onDestroySchema = useCallback(() => { if (!schema || !window.confirm('Вы уверены, что хотите удалить данную схему?')) { return; } destroyItem(schema.id, () => { toast.success('Схема удалена'); router.push('/library'); }); }, [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 (!activeID) { return; } const data: ICstUpdateData = { id: activeID, term_forms: forms }; cstUpdate(data, () => toast.success('Изменения сохранены')); }, [cstUpdate, activeID] ); return ( <> {loading ? : null} {error ? : null} {showUpload ? setShowUpload(false)} /> : null} {showClone ? setShowClone(false)} /> : null} {showCreateCst ? ( setShowCreateCst(false)} onCreate={handleCreateCst} schema={schema!} initial={createInitialData} /> ) : null} {showRenameCst ? ( setShowRenameCst(false)} onRename={handleRenameCst} initial={renameInitialData!} /> ) : null} {showDeleteCst ? ( setShowDeleteCst(false)} onDelete={handleDeleteCst} selected={toBeDeleted} /> ) : null} {showEditTerm ? ( setShowEditTerm(false)} onSave={handleSaveWordforms} target={activeCst!} /> ) : null} {showTemplates ? ( setShowTemplates(false)} insertAfter={insertCstID} onCreate={handleCreateCst} /> ) : null} {schema && !loading ? ( setShowUpload(true)} /> ) : null} ); } export default RSTabs; // ====== Internals ========= 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; }