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

319 lines
9.6 KiB
TypeScript
Raw Normal View History

'use client';
import axios from 'axios';
import clsx from 'clsx';
2023-09-11 20:31:54 +03:00
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
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
import { urls } from '@/app/urls';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import Divider from '@/components/ui/Divider';
import Loader from '@/components/ui/Loader';
import TabLabel from '@/components/ui/TabLabel';
import TextURL from '@/components/ui/TextURL';
2024-03-20 15:27:32 +03:00
import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext';
2023-12-26 14:23:51 +03:00
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
import { useRSForm } from '@/context/RSFormContext';
import useQueryStrings from '@/hooks/useQueryStrings';
2024-03-17 19:24:12 +03:00
import { ConstituentaID, IConstituenta, IConstituentaMeta } from '@/models/rsform';
2024-04-06 14:39:49 +03:00
import { PARAMETER, prefixes } from '@/utils/constants';
import { information, labelVersion, prompts } from '@/utils/labels';
import { OssTabID } from '../OssPage/OssTabs';
2023-07-28 00:03:37 +03:00
import EditorConstituenta from './EditorConstituenta';
2024-06-04 23:00:22 +03:00
import EditorRSForm from './EditorRSFormCard';
import EditorRSList from './EditorRSList';
2023-07-29 21:23:18 +03:00
import EditorTermGraph from './EditorTermGraph';
import MenuRSTabs from './MenuRSTabs';
import { RSEditState } from './RSEditContext';
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-07-27 22:04:25 +03:00
function RSTabs() {
const router = useConceptNavigation();
const query = useQueryStrings();
2024-07-28 11:38:40 +03:00
const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
const version = query.get('v') ? Number(query.get('v')) : undefined;
const cstQuery = query.get('active');
2024-04-01 19:07:20 +03:00
const { setNoFooter, calculateHeight } = useConceptOptions();
const { schema, loading, errorLoading, isArchive, itemID } = useRSForm();
const library = useLibrary();
2023-07-31 23:47:18 +03:00
const [isModified, setIsModified] = useState(false);
2023-12-13 15:03:58 +03:00
useBlockNavigation(isModified);
2023-08-31 17:25:42 +03:00
2024-03-17 19:24:12 +03:00
const [selected, setSelected] = useState<ConstituentaID[]>([]);
const activeCst: IConstituenta | undefined = useMemo(() => {
if (!schema || selected.length === 0) {
return undefined;
} else {
return schema.cstByID.get(selected.at(-1)!);
}
}, [schema, selected]);
2023-12-28 14:04:44 +03:00
2023-07-31 23:47:18 +03:00
useLayoutEffect(() => {
if (schema) {
2023-12-13 15:24:10 +03:00
const oldTitle = document.title;
document.title = schema.title;
2023-07-31 23:47:18 +03:00
return () => {
2023-12-13 15:24:10 +03:00
document.title = oldTitle;
2023-12-28 14:04:44 +03:00
};
2023-07-31 23:47:18 +03:00
}
2023-12-13 15:24:10 +03:00
}, [schema, schema?.title]);
useLayoutEffect(() => {
2023-12-16 19:20:26 +03:00
setNoFooter(activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST);
2023-12-01 02:57:19 +03:00
setIsModified(false);
if (activeTab === RSTabID.CST_EDIT) {
const cstID = Number(cstQuery);
if (cstID && schema && schema.cstByID.has(cstID)) {
setSelected([cstID]);
} else if (schema && schema?.items.length > 0) {
setSelected([schema.items[0].id]);
} else {
setSelected([]);
}
}
2023-08-27 15:39:49 +03:00
return () => setNoFooter(false);
}, [activeTab, cstQuery, setSelected, schema, setNoFooter, setIsModified]);
2023-12-16 19:20:26 +03:00
2023-09-05 00:23:53 +03:00
const navigateTab = useCallback(
2024-03-17 19:24:12 +03:00
(tab: RSTabID, activeID?: ConstituentaID) => {
2023-12-28 14:04:44 +03:00
if (!schema) {
return;
}
const url = urls.schema_props({
id: schema.id,
tab: tab,
active: activeID,
version: version
});
2023-12-28 14:04:44 +03:00
if (activeID) {
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
router.replace(url);
2023-12-28 14:04:44 +03:00
} else {
router.push(url);
2023-12-28 14:04:44 +03:00
}
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
activeID = schema.items[0].id;
router.replace(url);
} else {
router.push(url);
}
2023-12-28 14:04:44 +03:00
},
[router, schema, activeTab, version]
2023-12-28 14:04:44 +03:00
);
2024-05-29 11:31:38 +03:00
function onSelectTab(index: number, last: number, event: Event) {
if (last === index) {
return;
}
if (event.type == 'keydown') {
const kbEvent = event as KeyboardEvent;
if (kbEvent.altKey) {
if (kbEvent.code === 'ArrowLeft') {
router.back();
return;
} else if (kbEvent.code === 'ArrowRight') {
router.forward();
return;
}
}
}
navigateTab(index, selected.length > 0 ? selected.at(-1) : undefined);
}
const onCreateCst = useCallback(
(newCst: IConstituentaMeta) => {
navigateTab(activeTab, newCst.id);
2024-05-23 13:36:16 +03:00
if (activeTab === RSTabID.CST_LIST) {
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
2024-05-23 13:36:16 +03:00
inline: 'end'
});
}
2024-04-06 14:39:49 +03:00
}, PARAMETER.refreshTimeout);
2023-07-29 15:37:49 +03:00
}
2023-12-28 14:04:44 +03:00
},
[activeTab, navigateTab]
2023-12-28 14:04:44 +03:00
);
const onDeleteCst = useCallback(
2024-03-17 19:24:12 +03:00
(newActive?: ConstituentaID) => {
if (!newActive) {
navigateTab(RSTabID.CST_LIST);
} else if (activeTab === RSTabID.CST_EDIT) {
navigateTab(activeTab, newActive);
2023-12-28 14:04:44 +03:00
} else {
navigateTab(activeTab);
2023-12-28 14:04:44 +03:00
}
},
[activeTab, navigateTab]
2023-12-28 14:04:44 +03:00
);
const onOpenCst = useCallback(
2024-03-17 19:24:12 +03:00
(cstID: ConstituentaID) => {
2024-04-09 13:47:18 +03:00
if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) {
2024-04-08 11:47:47 +03:00
navigateTab(RSTabID.CST_EDIT, cstID);
}
2023-12-28 14:04:44 +03:00
},
2024-04-09 13:47:18 +03:00
[navigateTab, activeCst, activeTab]
2023-12-28 14:04:44 +03:00
);
2023-12-28 14:04:44 +03:00
const onDestroySchema = useCallback(() => {
if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
return;
}
const backToOSS = library.globalOSS?.schemas.includes(schema.id);
library.destroyItem(schema.id, () => {
toast.success(information.itemDestroyed);
if (backToOSS) {
router.push(urls.oss(library.globalOSS!.id, OssTabID.GRAPH));
} else {
router.push(urls.library);
}
});
}, [schema, library, router]);
2024-03-12 19:54:10 +03:00
const panelHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);
const cardPanel = useMemo(
() => (
2024-04-30 17:16:45 +03:00
<TabPanel>
<EditorRSForm
isModified={isModified} // prettier: split lines
setIsModified={setIsModified}
onDestroy={onDestroySchema}
/>
</TabPanel>
),
[isModified, onDestroySchema]
);
2024-04-30 17:16:45 +03:00
const listPanel = useMemo(
() => (
<TabPanel>
<EditorRSList onOpenEdit={onOpenCst} />
</TabPanel>
),
[onOpenCst]
);
const editorPanel = useMemo(
() => (
2024-04-30 17:16:45 +03:00
<TabPanel>
<EditorConstituenta
isModified={isModified}
setIsModified={setIsModified}
activeCst={activeCst}
onOpenEdit={onOpenCst}
/>
</TabPanel>
),
[isModified, setIsModified, activeCst, onOpenCst]
);
2024-04-30 17:16:45 +03:00
const graphPanel = useMemo(
() => (
<TabPanel>
<EditorTermGraph onOpenEdit={onOpenCst} />
</TabPanel>
),
[onOpenCst]
);
2023-12-28 14:04:44 +03:00
return (
<RSEditState
selected={selected}
setSelected={setSelected}
activeCst={activeCst}
isModified={isModified}
onCreateCst={onCreateCst}
onDeleteCst={onDeleteCst}
>
{loading ? <Loader /> : null}
{errorLoading ? <ProcessError error={errorLoading} isArchive={isArchive} itemID={itemID} /> : null}
2023-12-28 14:04:44 +03:00
{schema && !loading ? (
<Tabs
selectedIndex={activeTab}
onSelect={onSelectTab}
defaultFocus
selectedTabClassName='clr-selected'
className='flex flex-col mx-auto min-w-fit'
2023-12-28 14:04:44 +03:00
>
2024-03-06 21:33:59 +03:00
<TabList className={clsx('mx-auto w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2')}>
<MenuRSTabs onDestroy={onDestroySchema} />
<TabLabel
label='Карточка'
titleHtml={`Название: <b>${schema.title ?? ''}</b><br />Версия: ${labelVersion(schema)}`}
/>
<TabLabel
2023-12-28 14:04:44 +03:00
label='Содержание'
titleHtml={`Конституент: ${schema.stats?.count_all ?? 0}<br />Ошибок: ${schema.stats?.count_errors ?? 0}`}
2023-12-28 14:04:44 +03:00
/>
<TabLabel label='Редактор' />
<TabLabel label='Граф термов' />
2023-12-28 14:04:44 +03:00
</TabList>
2024-03-12 19:54:10 +03:00
<AnimateFade className='overflow-y-auto' style={{ maxHeight: panelHeight }}>
2024-04-30 17:16:45 +03:00
{cardPanel}
{listPanel}
{editorPanel}
{graphPanel}
2024-01-07 03:29:16 +03:00
</AnimateFade>
2023-12-28 14:04:44 +03:00
</Tabs>
) : null}
</RSEditState>
2023-12-28 14:04:44 +03:00
);
2023-07-15 17:46:19 +03:00
}
2023-07-27 22:04:25 +03:00
export default RSTabs;
// ====== Internals =========
function ProcessError({
error,
isArchive,
itemID
}: {
error: ErrorData;
isArchive: boolean;
itemID: string;
}): React.ReactElement {
if (axios.isAxiosError(error) && error.response) {
if (error.response.status === 404) {
return (
2024-06-05 19:42:48 +03:00
<div className='flex flex-col items-center p-2 mx-auto'>
2024-06-05 14:43:52 +03:00
<p>{`Концептуальная схема с указанным идентификатором ${isArchive ? 'и версией ' : ''}отсутствует`}</p>
<div className='flex justify-center'>
<TextURL text='Библиотека' href='/library' />
{isArchive ? <Divider vertical margins='mx-3' /> : null}
{isArchive ? <TextURL text='Актуальная версия' href={`/rsforms/${itemID}`} /> : null}
</div>
</div>
);
} else if (error.response.status === 403) {
return (
2024-06-05 19:42:48 +03:00
<div className='flex flex-col items-center p-2 mx-auto'>
<p>Владелец ограничил доступ к данной схеме</p>
<TextURL text='Библиотека' href='/library' />
</div>
);
}
}
return <InfoError error={error} />;
}