R: Split context and state dependencies to improve hot reload
This commit is contained in:
parent
e308a52b35
commit
531c44d3c8
|
@ -1,19 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, use, useEffect, useState } from 'react';
|
||||
import { createContext, use } from 'react';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
import { useLibrarySearchStore } from '@/features/library';
|
||||
import { useDeleteItem } from '@/features/library/backend/use-delete-item';
|
||||
import { RSTabID } from '@/features/rsform/pages/rsform-page/rsedit-context';
|
||||
import { useRoleStore, UserRole } from '@/features/users';
|
||||
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { promptText } from '@/utils/labels';
|
||||
|
||||
import { OperationType } from '../../backend/types';
|
||||
import { useOssSuspense } from '../../backend/use-oss';
|
||||
import { type IOperation, type IOperationSchema } from '../../models/oss';
|
||||
|
||||
export const OssTabID = {
|
||||
|
@ -37,7 +25,7 @@ export interface IOssEditContext {
|
|||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||
}
|
||||
|
||||
const OssEditContext = createContext<IOssEditContext | null>(null);
|
||||
export const OssEditContext = createContext<IOssEditContext | null>(null);
|
||||
export const useOssEdit = () => {
|
||||
const context = use(OssEditContext);
|
||||
if (context === null) {
|
||||
|
@ -45,97 +33,3 @@ export const useOssEdit = () => {
|
|||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface OssEditStateProps {
|
||||
itemID: number;
|
||||
}
|
||||
|
||||
export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEditStateProps>) => {
|
||||
const router = useConceptNavigation();
|
||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||
|
||||
const role = useRoleStore(state => state.role);
|
||||
const adjustRole = useRoleStore(state => state.adjustRole);
|
||||
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
||||
const searchLocation = useLibrarySearchStore(state => state.location);
|
||||
|
||||
const { user } = useAuthSuspense();
|
||||
const { schema } = useOssSuspense({ itemID: itemID });
|
||||
|
||||
const isOwned = !!user.id && user.id === schema.owner;
|
||||
const isMutable = role > UserRole.READER && !schema.read_only;
|
||||
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
|
||||
const { deleteItem } = useDeleteItem();
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
adjustRole({
|
||||
isOwner: isOwned,
|
||||
isEditor: !!user.id && schema.editors.includes(user.id),
|
||||
isStaff: user.is_staff,
|
||||
adminMode: adminMode
|
||||
}),
|
||||
[schema, adjustRole, isOwned, user, adminMode]
|
||||
);
|
||||
|
||||
function navigateTab(tab: OssTabID) {
|
||||
const url = urls.oss_props({
|
||||
id: schema.id,
|
||||
tab: tab
|
||||
});
|
||||
router.push({ path: url });
|
||||
}
|
||||
|
||||
function navigateOperationSchema(target: number) {
|
||||
const node = schema.operationByID.get(target);
|
||||
if (!node?.result) {
|
||||
return;
|
||||
}
|
||||
router.push({ path: urls.schema_props({ id: node.result, tab: RSTabID.CST_LIST }) });
|
||||
}
|
||||
|
||||
function deleteSchema() {
|
||||
if (!window.confirm(promptText.deleteOSS)) {
|
||||
return;
|
||||
}
|
||||
void deleteItem({
|
||||
target: schema.id,
|
||||
beforeInvalidate: () => {
|
||||
if (searchLocation === schema.location) {
|
||||
setSearchLocation('');
|
||||
}
|
||||
return router.pushAsync({ path: urls.library, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function canDeleteOperation(target: IOperation) {
|
||||
if (target.operation_type === OperationType.INPUT) {
|
||||
return true;
|
||||
}
|
||||
return schema.graph.expandOutputs([target.id]).length === 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<OssEditContext
|
||||
value={{
|
||||
schema,
|
||||
selected,
|
||||
|
||||
isOwned,
|
||||
isMutable,
|
||||
|
||||
navigateTab,
|
||||
navigateOperationSchema,
|
||||
|
||||
canDeleteOperation,
|
||||
deleteSchema,
|
||||
setSelected
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</OssEditContext>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
import { useLibrarySearchStore } from '@/features/library';
|
||||
import { useDeleteItem } from '@/features/library/backend/use-delete-item';
|
||||
import { RSTabID } from '@/features/rsform/pages/rsform-page/rsedit-context';
|
||||
import { useRoleStore, UserRole } from '@/features/users';
|
||||
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { promptText } from '@/utils/labels';
|
||||
|
||||
import { OperationType } from '../../backend/types';
|
||||
import { useOssSuspense } from '../../backend/use-oss';
|
||||
import { type IOperation } from '../../models/oss';
|
||||
|
||||
import { OssEditContext, type OssTabID } from './oss-edit-context';
|
||||
|
||||
interface OssEditStateProps {
|
||||
itemID: number;
|
||||
}
|
||||
|
||||
export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEditStateProps>) => {
|
||||
const router = useConceptNavigation();
|
||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||
|
||||
const role = useRoleStore(state => state.role);
|
||||
const adjustRole = useRoleStore(state => state.adjustRole);
|
||||
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
||||
const searchLocation = useLibrarySearchStore(state => state.location);
|
||||
|
||||
const { user } = useAuthSuspense();
|
||||
const { schema } = useOssSuspense({ itemID: itemID });
|
||||
|
||||
const isOwned = !!user.id && user.id === schema.owner;
|
||||
const isMutable = role > UserRole.READER && !schema.read_only;
|
||||
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
|
||||
const { deleteItem } = useDeleteItem();
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
adjustRole({
|
||||
isOwner: isOwned,
|
||||
isEditor: !!user.id && schema.editors.includes(user.id),
|
||||
isStaff: user.is_staff,
|
||||
adminMode: adminMode
|
||||
}),
|
||||
[schema, adjustRole, isOwned, user, adminMode]
|
||||
);
|
||||
|
||||
function navigateTab(tab: OssTabID) {
|
||||
const url = urls.oss_props({
|
||||
id: schema.id,
|
||||
tab: tab
|
||||
});
|
||||
router.push({ path: url });
|
||||
}
|
||||
|
||||
function navigateOperationSchema(target: number) {
|
||||
const node = schema.operationByID.get(target);
|
||||
if (!node?.result) {
|
||||
return;
|
||||
}
|
||||
router.push({ path: urls.schema_props({ id: node.result, tab: RSTabID.CST_LIST }) });
|
||||
}
|
||||
|
||||
function deleteSchema() {
|
||||
if (!window.confirm(promptText.deleteOSS)) {
|
||||
return;
|
||||
}
|
||||
void deleteItem({
|
||||
target: schema.id,
|
||||
beforeInvalidate: () => {
|
||||
if (searchLocation === schema.location) {
|
||||
setSearchLocation('');
|
||||
}
|
||||
return router.pushAsync({ path: urls.library, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function canDeleteOperation(target: IOperation) {
|
||||
if (target.operation_type === OperationType.INPUT) {
|
||||
return true;
|
||||
}
|
||||
return schema.graph.expandOutputs([target.id]).length === 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<OssEditContext
|
||||
value={{
|
||||
schema,
|
||||
selected,
|
||||
|
||||
isOwned,
|
||||
isMutable,
|
||||
|
||||
navigateTab,
|
||||
navigateOperationSchema,
|
||||
|
||||
canDeleteOperation,
|
||||
deleteSchema,
|
||||
setSelected
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</OssEditContext>
|
||||
);
|
||||
};
|
|
@ -16,7 +16,8 @@ import { useModificationStore } from '@/stores/modification';
|
|||
|
||||
import { OperationTooltip } from '../../components/operation-tooltip';
|
||||
|
||||
import { OssEditState, OssTabID } from './oss-edit-context';
|
||||
import { OssTabID } from './oss-edit-context';
|
||||
import { OssEditState } from './oss-edit-state';
|
||||
import { OssTabs } from './oss-tabs';
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
|
|
|
@ -19,7 +19,7 @@ import { FormCreateCst } from '../dlg-create-cst/form-create-cst';
|
|||
|
||||
import { TabArguments } from './tab-arguments';
|
||||
import { TabTemplate } from './tab-template';
|
||||
import { TemplateState } from './template-context';
|
||||
import { TemplateState } from './template-state';
|
||||
|
||||
export interface DlgCstTemplateProps {
|
||||
schema: IRSForm;
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, use, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { createContext, use } from 'react';
|
||||
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import { type ICstCreateDTO } from '../../backend/types';
|
||||
import { type IConstituenta } from '../../models/rsform';
|
||||
import { generateAlias } from '../../models/rsform-api';
|
||||
import { type IArgumentValue } from '../../models/rslang';
|
||||
import { inferTemplatedType, substituteTemplateArgs } from '../../models/rslang-api';
|
||||
|
||||
import { type DlgCstTemplateProps } from './dlg-cst-template';
|
||||
|
||||
export interface ITemplateContext {
|
||||
args: IArgumentValue[];
|
||||
|
@ -25,7 +17,7 @@ export interface ITemplateContext {
|
|||
onChangeFilterCategory: (newFilterCategory: IConstituenta | null) => void;
|
||||
}
|
||||
|
||||
const TemplateContext = createContext<ITemplateContext | null>(null);
|
||||
export const TemplateContext = createContext<ITemplateContext | null>(null);
|
||||
export const useTemplateContext = () => {
|
||||
const context = use(TemplateContext);
|
||||
if (context === null) {
|
||||
|
@ -33,63 +25,3 @@ export const useTemplateContext = () => {
|
|||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const TemplateState = ({ children }: React.PropsWithChildren) => {
|
||||
const { schema } = useDialogsStore(state => state.props as DlgCstTemplateProps);
|
||||
const { setValue } = useFormContext<ICstCreateDTO>();
|
||||
const [templateID, setTemplateID] = useState<number | null>(null);
|
||||
const [args, setArguments] = useState<IArgumentValue[]>([]);
|
||||
const [prototype, setPrototype] = useState<IConstituenta | null>(null);
|
||||
const [filterCategory, setFilterCategory] = useState<IConstituenta | null>(null);
|
||||
|
||||
function onChangeArguments(newArgs: IArgumentValue[]) {
|
||||
setArguments(newArgs);
|
||||
if (newArgs.length === 0 || !prototype) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newType = inferTemplatedType(prototype.cst_type, newArgs);
|
||||
setValue('definition_formal', substituteTemplateArgs(prototype.definition_formal, newArgs));
|
||||
setValue('cst_type', newType);
|
||||
setValue('alias', generateAlias(newType, schema));
|
||||
}
|
||||
|
||||
function onChangePrototype(newPrototype: IConstituenta) {
|
||||
setPrototype(newPrototype);
|
||||
setArguments(
|
||||
newPrototype.parse.args.map(arg => ({
|
||||
alias: arg.alias,
|
||||
typification: arg.typification,
|
||||
value: ''
|
||||
}))
|
||||
);
|
||||
setValue('cst_type', newPrototype.cst_type);
|
||||
setValue('alias', generateAlias(newPrototype.cst_type, schema));
|
||||
setValue('definition_formal', newPrototype.definition_formal);
|
||||
setValue('term_raw', newPrototype.term_raw);
|
||||
setValue('definition_raw', newPrototype.definition_raw);
|
||||
}
|
||||
|
||||
function onChangeTemplateID(newTemplateID: number | null) {
|
||||
setTemplateID(newTemplateID);
|
||||
setPrototype(null);
|
||||
setArguments([]);
|
||||
}
|
||||
|
||||
return (
|
||||
<TemplateContext
|
||||
value={{
|
||||
templateID,
|
||||
prototype,
|
||||
filterCategory,
|
||||
args,
|
||||
onChangeArguments,
|
||||
onChangePrototype,
|
||||
onChangeFilterCategory: setFilterCategory,
|
||||
onChangeTemplateID
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TemplateContext>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import { type ICstCreateDTO } from '../../backend/types';
|
||||
import { type IConstituenta } from '../../models/rsform';
|
||||
import { generateAlias } from '../../models/rsform-api';
|
||||
import { type IArgumentValue } from '../../models/rslang';
|
||||
import { inferTemplatedType, substituteTemplateArgs } from '../../models/rslang-api';
|
||||
|
||||
import { type DlgCstTemplateProps } from './dlg-cst-template';
|
||||
import { TemplateContext } from './template-context';
|
||||
|
||||
export const TemplateState = ({ children }: React.PropsWithChildren) => {
|
||||
const { schema } = useDialogsStore(state => state.props as DlgCstTemplateProps);
|
||||
const { setValue } = useFormContext<ICstCreateDTO>();
|
||||
const [templateID, setTemplateID] = useState<number | null>(null);
|
||||
const [args, setArguments] = useState<IArgumentValue[]>([]);
|
||||
const [prototype, setPrototype] = useState<IConstituenta | null>(null);
|
||||
const [filterCategory, setFilterCategory] = useState<IConstituenta | null>(null);
|
||||
|
||||
function onChangeArguments(newArgs: IArgumentValue[]) {
|
||||
setArguments(newArgs);
|
||||
if (newArgs.length === 0 || !prototype) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newType = inferTemplatedType(prototype.cst_type, newArgs);
|
||||
setValue('definition_formal', substituteTemplateArgs(prototype.definition_formal, newArgs));
|
||||
setValue('cst_type', newType);
|
||||
setValue('alias', generateAlias(newType, schema));
|
||||
}
|
||||
|
||||
function onChangePrototype(newPrototype: IConstituenta) {
|
||||
setPrototype(newPrototype);
|
||||
setArguments(
|
||||
newPrototype.parse.args.map(arg => ({
|
||||
alias: arg.alias,
|
||||
typification: arg.typification,
|
||||
value: ''
|
||||
}))
|
||||
);
|
||||
setValue('cst_type', newPrototype.cst_type);
|
||||
setValue('alias', generateAlias(newPrototype.cst_type, schema));
|
||||
setValue('definition_formal', newPrototype.definition_formal);
|
||||
setValue('term_raw', newPrototype.term_raw);
|
||||
setValue('definition_raw', newPrototype.definition_raw);
|
||||
}
|
||||
|
||||
function onChangeTemplateID(newTemplateID: number | null) {
|
||||
setTemplateID(newTemplateID);
|
||||
setPrototype(null);
|
||||
setArguments([]);
|
||||
}
|
||||
|
||||
return (
|
||||
<TemplateContext
|
||||
value={{
|
||||
templateID,
|
||||
prototype,
|
||||
filterCategory,
|
||||
args,
|
||||
onChangeArguments,
|
||||
onChangePrototype,
|
||||
onChangeFilterCategory: setFilterCategory,
|
||||
onChangeTemplateID
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TemplateContext>
|
||||
);
|
||||
};
|
|
@ -1,26 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, use, useEffect, useState } from 'react';
|
||||
import { createContext, use } from 'react';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
import { useLibrarySearchStore } from '@/features/library';
|
||||
import { useDeleteItem } from '@/features/library/backend/use-delete-item';
|
||||
import { useRoleStore, UserRole } from '@/features/users';
|
||||
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||
import { promptText } from '@/utils/labels';
|
||||
import { promptUnsaved } from '@/utils/utils';
|
||||
|
||||
import { CstType, type IConstituentaBasicsDTO, type ICstCreateDTO } from '../../backend/types';
|
||||
import { useCstCreate } from '../../backend/use-cst-create';
|
||||
import { useCstMove } from '../../backend/use-cst-move';
|
||||
import { useRSFormSuspense } from '../../backend/use-rsform';
|
||||
import { type CstType } from '../../backend/types';
|
||||
import { type IConstituenta, type IRSForm } from '../../models/rsform';
|
||||
import { generateAlias } from '../../models/rsform-api';
|
||||
|
||||
export const RSTabID = {
|
||||
CARD: 0,
|
||||
|
@ -67,7 +50,7 @@ export interface IRSEditContext {
|
|||
promptTemplate: () => void;
|
||||
}
|
||||
|
||||
const RSEditContext = createContext<IRSEditContext | null>(null);
|
||||
export const RSEditContext = createContext<IRSEditContext | null>(null);
|
||||
export const useRSEdit = () => {
|
||||
const context = use(RSEditContext);
|
||||
if (context === null) {
|
||||
|
@ -75,316 +58,3 @@ export const useRSEdit = () => {
|
|||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface RSEditStateProps {
|
||||
itemID: number;
|
||||
activeTab: RSTabID;
|
||||
activeVersion?: number;
|
||||
}
|
||||
|
||||
export const RSEditState = ({
|
||||
itemID,
|
||||
activeVersion,
|
||||
activeTab,
|
||||
children
|
||||
}: React.PropsWithChildren<RSEditStateProps>) => {
|
||||
const router = useConceptNavigation();
|
||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||
const role = useRoleStore(state => state.role);
|
||||
const adjustRole = useRoleStore(state => state.adjustRole);
|
||||
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
||||
const searchLocation = useLibrarySearchStore(state => state.location);
|
||||
|
||||
const { user } = useAuthSuspense();
|
||||
const { schema } = useRSFormSuspense({ itemID: itemID, version: activeVersion });
|
||||
const { isModified } = useModificationStore();
|
||||
|
||||
const isOwned = !!user.id && user.id === schema.owner;
|
||||
const isArchive = !!activeVersion;
|
||||
const isMutable = role > UserRole.READER && !schema.read_only;
|
||||
const isContentEditable = isMutable && !isArchive;
|
||||
const isAttachedToOSS = schema.oss.length > 0;
|
||||
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema.cstByID.get(id)?.is_inherited);
|
||||
const [focusCst, setFocusCst] = useState<IConstituenta | null>(null);
|
||||
|
||||
const activeCst = selected.length === 0 ? null : schema.cstByID.get(selected[selected.length - 1])!;
|
||||
|
||||
const { cstCreate } = useCstCreate();
|
||||
const { cstMove } = useCstMove();
|
||||
const { deleteItem } = useDeleteItem();
|
||||
|
||||
const showCreateCst = useDialogsStore(state => state.showCreateCst);
|
||||
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
||||
const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
adjustRole({
|
||||
isOwner: isOwned,
|
||||
isEditor: !!user.id && schema.editors.includes(user.id),
|
||||
isStaff: user.is_staff,
|
||||
adminMode: adminMode
|
||||
}),
|
||||
[schema, adjustRole, isOwned, user, adminMode]
|
||||
);
|
||||
|
||||
function handleSetFocus(newValue: IConstituenta | null) {
|
||||
setFocusCst(newValue);
|
||||
setSelected([]);
|
||||
}
|
||||
|
||||
function navigateVersion(versionID?: number) {
|
||||
router.push({ path: urls.schema(schema.id, versionID) });
|
||||
}
|
||||
|
||||
function navigateOss(ossID: number, newTab?: boolean) {
|
||||
router.push({ path: urls.oss(ossID), newTab: newTab });
|
||||
}
|
||||
|
||||
function navigateRSForm({ tab, activeID }: { tab: RSTabID; activeID?: number }) {
|
||||
const data = {
|
||||
id: schema.id,
|
||||
tab: tab,
|
||||
active: activeID,
|
||||
version: activeVersion
|
||||
};
|
||||
const url = urls.schema_props(data);
|
||||
if (activeID) {
|
||||
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
|
||||
router.replace({ path: url });
|
||||
} else {
|
||||
router.push({ path: url });
|
||||
}
|
||||
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
|
||||
data.active = schema.items[0].id;
|
||||
router.replace({ path: urls.schema_props(data) });
|
||||
} else {
|
||||
router.push({ path: url });
|
||||
}
|
||||
}
|
||||
|
||||
function navigateCst(cstID: number) {
|
||||
if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) {
|
||||
navigateRSForm({ tab: RSTabID.CST_EDIT, activeID: cstID });
|
||||
}
|
||||
}
|
||||
|
||||
function deleteSchema() {
|
||||
if (!window.confirm(promptText.deleteLibraryItem)) {
|
||||
return;
|
||||
}
|
||||
const ossID = schema.oss.length > 0 ? schema.oss[0].id : null;
|
||||
void deleteItem({
|
||||
target: schema.id,
|
||||
beforeInvalidate: () => {
|
||||
if (ossID) {
|
||||
return router.pushAsync({ path: urls.oss(ossID), force: true });
|
||||
} else {
|
||||
if (searchLocation === schema.location) {
|
||||
setSearchLocation('');
|
||||
}
|
||||
return router.pushAsync({ path: urls.library, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onCreateCst(newCst: IConstituentaBasicsDTO) {
|
||||
setSelected([newCst.id]);
|
||||
navigateRSForm({ tab: activeTab, activeID: newCst.id });
|
||||
if (activeTab === RSTabID.CST_LIST) {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`${prefixes.cst_list}${newCst.id}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'end'
|
||||
});
|
||||
}
|
||||
}, PARAMETER.refreshTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
function moveUp() {
|
||||
if (selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||
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);
|
||||
void cstMove({
|
||||
itemID: itemID,
|
||||
data: {
|
||||
items: selected,
|
||||
move_to: target
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function moveDown() {
|
||||
if (selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||
if (!selected.includes(cst.id)) {
|
||||
return prev;
|
||||
} else {
|
||||
count += 1;
|
||||
if (prev === -1) {
|
||||
return index;
|
||||
}
|
||||
return Math.max(prev, index);
|
||||
}
|
||||
}, -1);
|
||||
const target = Math.min(schema.items.length - 1, currentIndex - count + 2);
|
||||
void cstMove({
|
||||
itemID: itemID,
|
||||
data: {
|
||||
items: selected,
|
||||
move_to: target
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createCst(type: CstType | null, 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) {
|
||||
void cstCreate({ itemID: schema.id, data }).then(onCreateCst);
|
||||
} else {
|
||||
showCreateCst({ schema: schema, onCreate: onCreateCst, initial: data });
|
||||
}
|
||||
}
|
||||
|
||||
function cloneCst() {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
void cstCreate({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
insert_after: activeCst.id,
|
||||
cst_type: activeCst.cst_type,
|
||||
alias: generateAlias(activeCst.cst_type, schema),
|
||||
term_raw: activeCst.term_raw,
|
||||
definition_formal: activeCst.definition_formal,
|
||||
definition_raw: activeCst.definition_raw,
|
||||
convention: activeCst.convention,
|
||||
term_forms: activeCst.term_forms
|
||||
}
|
||||
}).then(onCreateCst);
|
||||
}
|
||||
|
||||
function promptDeleteCst() {
|
||||
showDeleteCst({
|
||||
schema: schema,
|
||||
selected: selected,
|
||||
afterDelete: (schema, deleted) => {
|
||||
const isEmpty = deleted.length === schema.items.length;
|
||||
const nextActive = isEmpty ? null : getNextActiveOnDelete(activeCst?.id ?? null, schema.items, deleted);
|
||||
setSelected(nextActive ? [nextActive] : []);
|
||||
if (!nextActive) {
|
||||
navigateRSForm({ tab: RSTabID.CST_LIST });
|
||||
} else if (activeTab === RSTabID.CST_EDIT) {
|
||||
navigateRSForm({ tab: activeTab, activeID: nextActive });
|
||||
} else {
|
||||
navigateRSForm({ tab: activeTab });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function promptTemplate() {
|
||||
if (isModified && !promptUnsaved()) {
|
||||
return;
|
||||
}
|
||||
showCstTemplate({ schema: schema, onCreate: onCreateCst, insertAfter: activeCst?.id });
|
||||
}
|
||||
|
||||
return (
|
||||
<RSEditContext
|
||||
value={{
|
||||
schema,
|
||||
focusCst,
|
||||
selected,
|
||||
activeCst,
|
||||
activeVersion,
|
||||
|
||||
isOwned,
|
||||
isArchive,
|
||||
isMutable,
|
||||
isContentEditable,
|
||||
isAttachedToOSS,
|
||||
canDeleteSelected,
|
||||
|
||||
navigateVersion,
|
||||
navigateRSForm,
|
||||
navigateCst,
|
||||
navigateOss,
|
||||
|
||||
deleteSchema,
|
||||
|
||||
setFocus: handleSetFocus,
|
||||
setSelected,
|
||||
select: (target: number) => setSelected(prev => [...prev, target]),
|
||||
deselect: (target: number) => setSelected(prev => prev.filter(id => id !== target)),
|
||||
toggleSelect: (target: number) =>
|
||||
setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
|
||||
deselectAll: () => setSelected([]),
|
||||
|
||||
moveUp,
|
||||
moveDown,
|
||||
createCst,
|
||||
createCstDefault: () => createCst(null, false),
|
||||
cloneCst,
|
||||
promptDeleteCst,
|
||||
|
||||
promptTemplate
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RSEditContext>
|
||||
);
|
||||
};
|
||||
|
||||
// ====== Internals =========
|
||||
function getNextActiveOnDelete(activeID: number | null, items: IConstituenta[], deleted: number[]): number | null {
|
||||
if (items.length === deleted.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let activeIndex = items.findIndex(cst => cst.id === activeID);
|
||||
if (activeIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,338 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
import { useLibrarySearchStore } from '@/features/library';
|
||||
import { useDeleteItem } from '@/features/library/backend/use-delete-item';
|
||||
import { useRoleStore, UserRole } from '@/features/users';
|
||||
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||
import { promptText } from '@/utils/labels';
|
||||
import { promptUnsaved } from '@/utils/utils';
|
||||
|
||||
import { CstType, type IConstituentaBasicsDTO, type ICstCreateDTO } from '../../backend/types';
|
||||
import { useCstCreate } from '../../backend/use-cst-create';
|
||||
import { useCstMove } from '../../backend/use-cst-move';
|
||||
import { useRSFormSuspense } from '../../backend/use-rsform';
|
||||
import { type IConstituenta } from '../../models/rsform';
|
||||
import { generateAlias } from '../../models/rsform-api';
|
||||
|
||||
import { RSEditContext, RSTabID } from './rsedit-context';
|
||||
|
||||
interface RSEditStateProps {
|
||||
itemID: number;
|
||||
activeTab: RSTabID;
|
||||
activeVersion?: number;
|
||||
}
|
||||
|
||||
export const RSEditState = ({
|
||||
itemID,
|
||||
activeVersion,
|
||||
activeTab,
|
||||
children
|
||||
}: React.PropsWithChildren<RSEditStateProps>) => {
|
||||
const router = useConceptNavigation();
|
||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||
const role = useRoleStore(state => state.role);
|
||||
const adjustRole = useRoleStore(state => state.adjustRole);
|
||||
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
||||
const searchLocation = useLibrarySearchStore(state => state.location);
|
||||
|
||||
const { user } = useAuthSuspense();
|
||||
const { schema } = useRSFormSuspense({ itemID: itemID, version: activeVersion });
|
||||
const { isModified } = useModificationStore();
|
||||
|
||||
const isOwned = !!user.id && user.id === schema.owner;
|
||||
const isArchive = !!activeVersion;
|
||||
const isMutable = role > UserRole.READER && !schema.read_only;
|
||||
const isContentEditable = isMutable && !isArchive;
|
||||
const isAttachedToOSS = schema.oss.length > 0;
|
||||
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema.cstByID.get(id)?.is_inherited);
|
||||
const [focusCst, setFocusCst] = useState<IConstituenta | null>(null);
|
||||
|
||||
const activeCst = selected.length === 0 ? null : schema.cstByID.get(selected[selected.length - 1])!;
|
||||
|
||||
const { cstCreate } = useCstCreate();
|
||||
const { cstMove } = useCstMove();
|
||||
const { deleteItem } = useDeleteItem();
|
||||
|
||||
const showCreateCst = useDialogsStore(state => state.showCreateCst);
|
||||
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
||||
const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
adjustRole({
|
||||
isOwner: isOwned,
|
||||
isEditor: !!user.id && schema.editors.includes(user.id),
|
||||
isStaff: user.is_staff,
|
||||
adminMode: adminMode
|
||||
}),
|
||||
[schema, adjustRole, isOwned, user, adminMode]
|
||||
);
|
||||
|
||||
function handleSetFocus(newValue: IConstituenta | null) {
|
||||
setFocusCst(newValue);
|
||||
setSelected([]);
|
||||
}
|
||||
|
||||
function navigateVersion(versionID?: number) {
|
||||
router.push({ path: urls.schema(schema.id, versionID) });
|
||||
}
|
||||
|
||||
function navigateOss(ossID: number, newTab?: boolean) {
|
||||
router.push({ path: urls.oss(ossID), newTab: newTab });
|
||||
}
|
||||
|
||||
function navigateRSForm({ tab, activeID }: { tab: RSTabID; activeID?: number }) {
|
||||
const data = {
|
||||
id: schema.id,
|
||||
tab: tab,
|
||||
active: activeID,
|
||||
version: activeVersion
|
||||
};
|
||||
const url = urls.schema_props(data);
|
||||
if (activeID) {
|
||||
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
|
||||
router.replace({ path: url });
|
||||
} else {
|
||||
router.push({ path: url });
|
||||
}
|
||||
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
|
||||
data.active = schema.items[0].id;
|
||||
router.replace({ path: urls.schema_props(data) });
|
||||
} else {
|
||||
router.push({ path: url });
|
||||
}
|
||||
}
|
||||
|
||||
function navigateCst(cstID: number) {
|
||||
if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) {
|
||||
navigateRSForm({ tab: RSTabID.CST_EDIT, activeID: cstID });
|
||||
}
|
||||
}
|
||||
|
||||
function deleteSchema() {
|
||||
if (!window.confirm(promptText.deleteLibraryItem)) {
|
||||
return;
|
||||
}
|
||||
const ossID = schema.oss.length > 0 ? schema.oss[0].id : null;
|
||||
void deleteItem({
|
||||
target: schema.id,
|
||||
beforeInvalidate: () => {
|
||||
if (ossID) {
|
||||
return router.pushAsync({ path: urls.oss(ossID), force: true });
|
||||
} else {
|
||||
if (searchLocation === schema.location) {
|
||||
setSearchLocation('');
|
||||
}
|
||||
return router.pushAsync({ path: urls.library, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onCreateCst(newCst: IConstituentaBasicsDTO) {
|
||||
setSelected([newCst.id]);
|
||||
navigateRSForm({ tab: activeTab, activeID: newCst.id });
|
||||
if (activeTab === RSTabID.CST_LIST) {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`${prefixes.cst_list}${newCst.id}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'end'
|
||||
});
|
||||
}
|
||||
}, PARAMETER.refreshTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
function moveUp() {
|
||||
if (selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||
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);
|
||||
void cstMove({
|
||||
itemID: itemID,
|
||||
data: {
|
||||
items: selected,
|
||||
move_to: target
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function moveDown() {
|
||||
if (selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||
if (!selected.includes(cst.id)) {
|
||||
return prev;
|
||||
} else {
|
||||
count += 1;
|
||||
if (prev === -1) {
|
||||
return index;
|
||||
}
|
||||
return Math.max(prev, index);
|
||||
}
|
||||
}, -1);
|
||||
const target = Math.min(schema.items.length - 1, currentIndex - count + 2);
|
||||
void cstMove({
|
||||
itemID: itemID,
|
||||
data: {
|
||||
items: selected,
|
||||
move_to: target
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createCst(type: CstType | null, 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) {
|
||||
void cstCreate({ itemID: schema.id, data }).then(onCreateCst);
|
||||
} else {
|
||||
showCreateCst({ schema: schema, onCreate: onCreateCst, initial: data });
|
||||
}
|
||||
}
|
||||
|
||||
function cloneCst() {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
void cstCreate({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
insert_after: activeCst.id,
|
||||
cst_type: activeCst.cst_type,
|
||||
alias: generateAlias(activeCst.cst_type, schema),
|
||||
term_raw: activeCst.term_raw,
|
||||
definition_formal: activeCst.definition_formal,
|
||||
definition_raw: activeCst.definition_raw,
|
||||
convention: activeCst.convention,
|
||||
term_forms: activeCst.term_forms
|
||||
}
|
||||
}).then(onCreateCst);
|
||||
}
|
||||
|
||||
function promptDeleteCst() {
|
||||
showDeleteCst({
|
||||
schema: schema,
|
||||
selected: selected,
|
||||
afterDelete: (schema, deleted) => {
|
||||
const isEmpty = deleted.length === schema.items.length;
|
||||
const nextActive = isEmpty ? null : getNextActiveOnDelete(activeCst?.id ?? null, schema.items, deleted);
|
||||
setSelected(nextActive ? [nextActive] : []);
|
||||
if (!nextActive) {
|
||||
navigateRSForm({ tab: RSTabID.CST_LIST });
|
||||
} else if (activeTab === RSTabID.CST_EDIT) {
|
||||
navigateRSForm({ tab: activeTab, activeID: nextActive });
|
||||
} else {
|
||||
navigateRSForm({ tab: activeTab });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function promptTemplate() {
|
||||
if (isModified && !promptUnsaved()) {
|
||||
return;
|
||||
}
|
||||
showCstTemplate({ schema: schema, onCreate: onCreateCst, insertAfter: activeCst?.id });
|
||||
}
|
||||
|
||||
return (
|
||||
<RSEditContext
|
||||
value={{
|
||||
schema,
|
||||
focusCst,
|
||||
selected,
|
||||
activeCst,
|
||||
activeVersion,
|
||||
|
||||
isOwned,
|
||||
isArchive,
|
||||
isMutable,
|
||||
isContentEditable,
|
||||
isAttachedToOSS,
|
||||
canDeleteSelected,
|
||||
|
||||
navigateVersion,
|
||||
navigateRSForm,
|
||||
navigateCst,
|
||||
navigateOss,
|
||||
|
||||
deleteSchema,
|
||||
|
||||
setFocus: handleSetFocus,
|
||||
setSelected,
|
||||
select: (target: number) => setSelected(prev => [...prev, target]),
|
||||
deselect: (target: number) => setSelected(prev => prev.filter(id => id !== target)),
|
||||
toggleSelect: (target: number) =>
|
||||
setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
|
||||
deselectAll: () => setSelected([]),
|
||||
|
||||
moveUp,
|
||||
moveDown,
|
||||
createCst,
|
||||
createCstDefault: () => createCst(null, false),
|
||||
cloneCst,
|
||||
promptDeleteCst,
|
||||
|
||||
promptTemplate
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RSEditContext>
|
||||
);
|
||||
};
|
||||
|
||||
// ====== Internals =========
|
||||
function getNextActiveOnDelete(activeID: number | null, items: IConstituenta[], deleted: number[]): number | null {
|
||||
if (items.length === deleted.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let activeIndex = items.findIndex(cst => cst.id === activeID);
|
||||
if (activeIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -16,7 +16,8 @@ import { useModificationStore } from '@/stores/modification';
|
|||
|
||||
import { ConstituentaTooltip } from '../../components/constituenta-tooltip';
|
||||
|
||||
import { RSEditState, RSTabID } from './rsedit-context';
|
||||
import { RSTabID } from './rsedit-context';
|
||||
import { RSEditState } from './rsedit-state';
|
||||
import { RSTabs } from './rstabs';
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
|
|
Loading…
Reference in New Issue
Block a user