Portal/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx

386 lines
11 KiB
TypeScript
Raw Normal View History

2024-06-07 20:17:03 +03:00
'use client';
2025-01-23 19:41:31 +03:00
import { createContext, useContext, useEffect, useState } from 'react';
2024-06-07 20:17:03 +03:00
2025-01-23 19:41:31 +03:00
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
2024-06-07 20:17:03 +03:00
import { urls } from '@/app/urls';
import { useAuthSuspense } from '@/backend/auth/useAuth';
2025-01-26 22:24:34 +03:00
import { useDeleteItem } from '@/backend/library/useDeleteItem';
import { ICstCreateDTO } from '@/backend/rsform/api';
2025-01-23 19:41:31 +03:00
import { useCstCreate } from '@/backend/rsform/useCstCreate';
import { useCstDelete } from '@/backend/rsform/useCstDelete';
import { useCstMove } from '@/backend/rsform/useCstMove';
import { useRSFormSuspense } from '@/backend/rsform/useRSForm';
2025-01-26 22:24:34 +03:00
import { ILibraryItemEditor, LibraryItemID, VersionID } from '@/models/library';
import { ConstituentaID, CstType, IConstituenta, IRSForm } from '@/models/rsform';
2024-06-07 20:17:03 +03:00
import { generateAlias } from '@/models/rsformAPI';
2025-01-26 22:24:34 +03:00
import { UserRole } from '@/models/user';
import { useDialogsStore } from '@/stores/dialogs';
2025-01-26 22:24:34 +03:00
import { useModificationStore } from '@/stores/modification';
import { usePreferencesStore } from '@/stores/preferences';
2025-01-15 23:03:23 +03:00
import { useRoleStore } from '@/stores/role';
2025-01-26 22:24:34 +03:00
import { PARAMETER, prefixes } from '@/utils/constants';
import { prompts } from '@/utils/labels';
2024-06-07 20:17:03 +03:00
import { promptUnsaved } from '@/utils/utils';
2025-01-26 22:24:34 +03:00
import { OssTabID } from '../OssPage/OssEditContext';
export enum RSTabID {
CARD = 0,
CST_LIST = 1,
CST_EDIT = 2,
TERM_GRAPH = 3
}
export interface IRSEditContext extends ILibraryItemEditor {
2025-01-26 22:24:34 +03:00
schema: IRSForm;
2024-06-07 20:17:03 +03:00
selected: ConstituentaID[];
2025-01-26 22:24:34 +03:00
activeCst?: IConstituenta;
activeVersion?: VersionID;
2024-06-07 20:17:03 +03:00
2025-01-23 19:41:31 +03:00
isOwned: boolean;
isArchive: boolean;
2024-06-07 20:17:03 +03:00
isMutable: boolean;
isContentEditable: boolean;
isAttachedToOSS: boolean;
2024-08-01 11:55:45 +03:00
canDeleteSelected: boolean;
2024-06-07 20:17:03 +03:00
navigateVersion: (versionID: VersionID | undefined) => void;
2025-01-26 22:24:34 +03:00
navigateRSForm: ({ tab, activeID }: { tab: RSTabID; activeID?: ConstituentaID }) => void;
navigateCst: (cstID: ConstituentaID) => void;
navigateOss: (target: LibraryItemID, newTab?: boolean) => void;
deleteSchema: () => void;
2024-06-07 20:17:03 +03:00
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
select: (target: ConstituentaID) => void;
deselect: (target: ConstituentaID) => void;
toggleSelect: (target: ConstituentaID) => void;
deselectAll: () => void;
moveUp: () => void;
moveDown: () => void;
createCst: (type: CstType | undefined, skipDialog: boolean, definition?: string) => void;
cloneCst: () => void;
2024-08-01 11:55:45 +03:00
promptDeleteCst: () => void;
2024-06-07 20:17:03 +03:00
promptTemplate: () => void;
}
const RSEditContext = createContext<IRSEditContext | null>(null);
export const useRSEdit = () => {
const context = useContext(RSEditContext);
if (context === null) {
2024-12-12 21:58:07 +03:00
throw new Error('useRSEdit has to be used within <RSEditState>');
2024-06-07 20:17:03 +03:00
}
return context;
};
interface RSEditStateProps {
2025-01-23 19:41:31 +03:00
itemID: LibraryItemID;
2025-01-26 22:24:34 +03:00
activeTab: RSTabID;
activeVersion?: VersionID;
2024-06-07 20:17:03 +03:00
}
export const RSEditState = ({
itemID,
activeVersion,
activeTab,
children
}: React.PropsWithChildren<RSEditStateProps>) => {
2024-06-07 20:17:03 +03:00
const router = useConceptNavigation();
const adminMode = usePreferencesStore(state => state.adminMode);
2025-01-15 23:03:23 +03:00
const role = useRoleStore(state => state.role);
const adjustRole = useRoleStore(state => state.adjustRole);
2024-06-07 20:17:03 +03:00
const { user } = useAuthSuspense();
const { schema } = useRSFormSuspense({ itemID: itemID, version: activeVersion });
2025-01-26 22:24:34 +03:00
const { isModified } = useModificationStore();
2025-01-23 19:41:31 +03:00
const isOwned = !!user.id && user.id === schema.owner;
const isArchive = !!activeVersion;
2025-01-26 22:24:34 +03:00
const isMutable = role > UserRole.READER && !schema.read_only;
2025-01-23 19:41:31 +03:00
const isContentEditable = isMutable && !isArchive;
2025-01-26 22:24:34 +03:00
const isAttachedToOSS = schema.oss.length > 0;
2025-01-23 19:41:31 +03:00
2025-01-26 22:24:34 +03:00
const [selected, setSelected] = useState<ConstituentaID[]>([]);
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema.cstByID.get(id)?.is_inherited);
2024-06-07 20:17:03 +03:00
const activeCst = selected.length === 0 ? undefined : schema.cstByID.get(selected[selected.length - 1]);
2025-01-26 22:24:34 +03:00
const { cstCreate } = useCstCreate();
const { cstMove } = useCstMove();
const { cstDelete } = useCstDelete();
const { deleteItem } = useDeleteItem();
const showCreateCst = useDialogsStore(state => state.showCreateCst);
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
2024-11-20 00:32:26 +03:00
useEffect(
2024-06-07 20:17:03 +03:00
() =>
2025-01-15 23:03:23 +03:00
adjustRole({
2025-01-23 19:41:31 +03:00
isOwner: isOwned,
isEditor: !!user.id && schema.editors.includes(user.id),
isStaff: user.is_staff,
2025-01-15 23:03:23 +03:00
adminMode: adminMode
2024-06-07 20:17:03 +03:00
}),
2025-01-23 19:41:31 +03:00
[schema, adjustRole, isOwned, user, adminMode]
2024-06-07 20:17:03 +03:00
);
function navigateVersion(versionID: VersionID | undefined) {
router.push(urls.schema(schema.id, versionID));
}
2025-01-26 22:24:34 +03:00
function navigateOss(target: LibraryItemID, newTab?: boolean) {
2025-01-23 19:41:31 +03:00
router.push(urls.oss(target), newTab);
}
2024-08-01 00:35:49 +03:00
2025-01-26 22:24:34 +03:00
function navigateRSForm({ tab, activeID }: { tab: RSTabID; activeID?: ConstituentaID }) {
if (!schema) {
2024-06-07 20:17:03 +03:00
return;
}
2025-01-26 22:24:34 +03:00
const data = {
id: schema.id,
tab: tab,
active: activeID,
version: activeVersion
2025-01-26 22:24:34 +03:00
};
const url = urls.schema_props(data);
if (activeID) {
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
router.replace(url);
} else {
router.push(url);
}
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
data.active = schema.items[0].id;
router.replace(urls.schema_props(data));
} else {
router.push(url);
}
2025-01-23 19:41:31 +03:00
}
2024-06-07 20:17:03 +03:00
2025-01-26 22:24:34 +03:00
function navigateCst(cstID: ConstituentaID) {
if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) {
navigateRSForm({ tab: RSTabID.CST_EDIT, activeID: cstID });
2024-06-07 20:17:03 +03:00
}
2025-01-23 19:41:31 +03:00
}
2024-06-07 20:17:03 +03:00
2025-01-26 22:24:34 +03:00
function deleteSchema() {
if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
2025-01-23 19:41:31 +03:00
return;
}
2025-01-26 22:24:34 +03:00
const ossID = schema.oss.length > 0 ? schema.oss[0].id : undefined;
deleteItem(schema.id, () => {
if (ossID) {
router.push(urls.oss(ossID, OssTabID.GRAPH));
} else {
router.push(urls.library);
}
});
}
function handleCreateCst(data: ICstCreateDTO) {
2025-01-23 19:41:31 +03:00
data.alias = data.alias || generateAlias(data.cst_type, schema);
cstCreate({ itemID: itemID, data }, newCst => {
setSelected([newCst.id]);
2025-01-26 22:24:34 +03:00
navigateRSForm({ tab: activeTab, activeID: newCst.id });
if (activeTab === RSTabID.CST_LIST) {
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'end'
});
}
}, PARAMETER.refreshTimeout);
}
2025-01-23 19:41:31 +03:00
});
}
2024-06-07 20:17:03 +03:00
2025-01-23 19:41:31 +03:00
function handleDeleteCst(deleted: ConstituentaID[]) {
const data = {
items: deleted
};
2024-06-07 20:17:03 +03:00
2025-01-23 19:41:31 +03:00
const isEmpty = deleted.length === schema.items.length;
const nextActive = isEmpty ? undefined : getNextActiveOnDelete(activeCst?.id, schema.items, deleted);
2024-06-07 20:17:03 +03:00
2025-01-23 19:41:31 +03:00
cstDelete({ itemID: itemID, data }, () => {
setSelected(nextActive ? [nextActive] : []);
2025-01-26 22:24:34 +03:00
if (!nextActive) {
navigateRSForm({ tab: RSTabID.CST_LIST });
} else if (activeTab === RSTabID.CST_EDIT) {
navigateRSForm({ tab: activeTab, activeID: nextActive });
} else {
navigateRSForm({ tab: activeTab });
2024-06-07 20:17:03 +03:00
}
2025-01-23 19:41:31 +03:00
});
}
2024-06-07 20:17:03 +03:00
2025-01-23 19:41:31 +03:00
function moveUp() {
2025-01-26 22:24:34 +03:00
if (!schema.items || selected.length === 0) {
2024-06-07 20:17:03 +03:00
return;
}
2025-01-23 19:41:31 +03:00
const currentIndex = schema.items.reduce((prev, cst, index) => {
2024-06-07 20:17:03 +03:00
if (!selected.includes(cst.id)) {
return prev;
} else if (prev === -1) {
return index;
}
return Math.min(prev, index);
}, -1);
const target = Math.max(0, currentIndex - 1);
2025-01-23 19:41:31 +03:00
cstMove({
itemID: itemID,
data: {
items: selected,
move_to: target
}
});
}
2024-06-07 20:17:03 +03:00
2025-01-23 19:41:31 +03:00
function moveDown() {
2025-01-26 22:24:34 +03:00
if (!schema.items || selected.length === 0) {
2024-06-07 20:17:03 +03:00
return;
}
let count = 0;
2025-01-23 19:41:31 +03:00
const currentIndex = schema.items.reduce((prev, cst, index) => {
2024-06-07 20:17:03 +03:00
if (!selected.includes(cst.id)) {
return prev;
} else {
count += 1;
if (prev === -1) {
return index;
}
return Math.max(prev, index);
}
}, -1);
2025-01-23 19:41:31 +03:00
const target = Math.min(schema.items.length - 1, currentIndex - count + 2);
cstMove({
itemID: itemID,
data: {
items: selected,
move_to: target
2024-08-29 12:41:59 +03:00
}
2025-01-23 19:41:31 +03:00
});
}
function createCst(type: CstType | undefined, skipDialog: boolean, definition?: string) {
const targetType = type ?? activeCst?.cst_type ?? CstType.BASE;
const data: ICstCreateDTO = {
insert_after: activeCst?.id ?? null,
cst_type: targetType,
alias: generateAlias(targetType, schema),
term_raw: '',
definition_formal: definition ?? '',
definition_raw: '',
convention: '',
term_forms: []
};
if (skipDialog) {
handleCreateCst(data);
} else {
showCreateCst({ schema: schema, onCreate: handleCreateCst, initial: data });
}
}
2024-06-07 20:17:03 +03:00
2025-01-23 19:41:31 +03:00
function cloneCst() {
2025-01-26 22:24:34 +03:00
if (!activeCst) {
2024-06-07 20:17:03 +03:00
return;
}
2025-01-23 19:41:31 +03:00
const data: ICstCreateDTO = {
2024-06-07 20:17:03 +03:00
insert_after: activeCst.id,
cst_type: activeCst.cst_type,
2025-01-23 19:41:31 +03:00
alias: generateAlias(activeCst.cst_type, schema),
2024-06-07 20:17:03 +03:00
term_raw: activeCst.term_raw,
definition_formal: activeCst.definition_formal,
definition_raw: activeCst.definition_raw,
convention: activeCst.convention,
term_forms: activeCst.term_forms
};
handleCreateCst(data);
2025-01-23 19:41:31 +03:00
}
2024-06-07 20:17:03 +03:00
2025-01-23 19:41:31 +03:00
function promptDeleteCst() {
showDeleteCst({ schema: schema, selected: selected, onDelete: handleDeleteCst });
}
function promptTemplate() {
2025-01-26 22:24:34 +03:00
if (isModified && !promptUnsaved()) {
2024-06-07 20:17:03 +03:00
return;
}
2025-01-23 19:41:31 +03:00
showCstTemplate({ schema: schema, onCreate: handleCreateCst, insertAfter: activeCst?.id });
}
2024-06-07 20:17:03 +03:00
return (
2024-12-12 21:58:07 +03:00
<RSEditContext
2024-06-07 20:17:03 +03:00
value={{
2025-01-26 22:24:34 +03:00
schema,
2024-06-07 20:17:03 +03:00
selected,
2025-01-26 22:24:34 +03:00
activeCst,
activeVersion,
2025-01-23 19:41:31 +03:00
isOwned,
isArchive,
2024-06-07 20:17:03 +03:00
isMutable,
isContentEditable,
isAttachedToOSS,
2024-08-01 11:55:45 +03:00
canDeleteSelected,
2024-06-07 20:17:03 +03:00
navigateVersion,
2025-01-26 22:24:34 +03:00
navigateRSForm,
navigateCst,
navigateOss,
2025-01-26 22:24:34 +03:00
deleteSchema,
2024-06-07 20:17:03 +03:00
2025-01-26 22:24:34 +03:00
setSelected,
2024-06-07 20:17:03 +03:00
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
toggleSelect: (target: ConstituentaID) =>
setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
deselectAll: () => setSelected([]),
moveUp,
moveDown,
createCst,
cloneCst,
promptDeleteCst,
2025-01-26 22:24:34 +03:00
promptTemplate
2024-06-07 20:17:03 +03:00
}}
>
{children}
2024-12-12 21:58:07 +03:00
</RSEditContext>
2024-06-07 20:17:03 +03:00
);
};
// ====== Internals =========
function getNextActiveOnDelete(
activeID: ConstituentaID | undefined,
items: IConstituenta[],
deleted: ConstituentaID[]
): ConstituentaID | 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;
}