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

735 lines
22 KiB
TypeScript
Raw Normal View History

2024-06-07 20:17:03 +03:00
'use client';
import fileDownload from 'js-file-download';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
2024-06-07 20:17:03 +03:00
import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
2025-01-21 20:33:05 +03:00
import { useAuth } from '@/backend/auth/useAuth';
2024-06-27 14:43:06 +03:00
import { useConceptNavigation } from '@/context/NavigationContext';
2024-06-07 20:17:03 +03:00
import { useRSForm } from '@/context/RSFormContext';
2024-08-01 00:35:49 +03:00
import {
AccessPolicy,
ILibraryItemEditor,
2024-08-01 00:35:49 +03:00
ILibraryUpdateData,
IVersionData,
LibraryItemID,
LocationHead,
VersionID
} from '@/models/library';
2024-07-20 18:26:32 +03:00
import { ICstSubstituteData } from '@/models/oss';
2024-06-07 20:17:03 +03:00
import {
ConstituentaID,
CstType,
IConstituenta,
IConstituentaMeta,
ICstCreateData,
ICstMovetoData,
ICstRenameData,
ICstUpdateData,
IInlineSynthesisData,
IRSForm,
ITargetCst,
TermForm
} from '@/models/rsform';
import { generateAlias } from '@/models/rsformAPI';
2025-01-15 23:03:23 +03:00
import { UserID, UserRole } from '@/models/user';
import { useDialogsStore } from '@/stores/dialogs';
import { usePreferencesStore } from '@/stores/preferences';
2025-01-15 23:03:23 +03:00
import { useRoleStore } from '@/stores/role';
2024-06-07 20:17:03 +03:00
import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { information, prompts } from '@/utils/labels';
import { promptUnsaved } from '@/utils/utils';
import { RSTabID } from './RSTabs';
export interface IRSEditContext extends ILibraryItemEditor {
2024-06-07 20:17:03 +03:00
schema?: IRSForm;
selected: ConstituentaID[];
isMutable: boolean;
isContentEditable: boolean;
isProcessing: boolean;
isAttachedToOSS: boolean;
2024-06-07 20:17:03 +03:00
canProduceStructure: boolean;
2024-08-01 11:55:45 +03:00
canDeleteSelected: boolean;
2024-06-07 20:17:03 +03:00
2024-08-01 00:35:49 +03:00
updateSchema: (data: ILibraryUpdateData) => void;
2024-06-07 20:17:03 +03:00
setOwner: (newOwner: UserID) => void;
setAccessPolicy: (newPolicy: AccessPolicy) => void;
promptEditors: () => void;
promptLocation: () => void;
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
select: (target: ConstituentaID) => void;
deselect: (target: ConstituentaID) => void;
toggleSelect: (target: ConstituentaID) => void;
deselectAll: () => void;
2024-08-01 00:35:49 +03:00
viewOSS: (target: LibraryItemID, newTab?: boolean) => void;
2024-06-07 20:17:03 +03:00
viewVersion: (version?: VersionID, newTab?: boolean) => void;
viewPredecessor: (target: ConstituentaID) => void;
2024-06-07 20:17:03 +03:00
createVersion: () => void;
restoreVersion: () => void;
2024-08-01 11:55:45 +03:00
promptEditVersions: () => void;
2024-06-07 20:17:03 +03:00
moveUp: () => void;
moveDown: () => void;
createCst: (type: CstType | undefined, skipDialog: boolean, definition?: string) => void;
renameCst: () => void;
cloneCst: () => void;
2024-08-01 11:55:45 +03:00
promptDeleteCst: () => void;
2024-06-07 20:17:03 +03:00
editTermForms: () => void;
promptTemplate: () => void;
promptClone: () => void;
promptUpload: () => void;
share: () => void;
download: () => void;
reindex: () => void;
reorder: () => void;
produceStructure: () => void;
inlineSynthesis: () => void;
substitute: () => void;
2024-11-20 00:32:26 +03:00
showTypeGraph: () => void;
2024-12-18 12:35:17 +03:00
showQR: () => void;
2024-06-07 20:17:03 +03:00
}
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 {
selected: ConstituentaID[];
isModified: boolean;
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
activeCst?: IConstituenta;
onCreateCst?: (newCst: IConstituentaMeta) => void;
onDeleteCst?: (newActive?: ConstituentaID) => void;
}
export const RSEditState = ({
selected,
setSelected,
activeCst,
isModified,
onCreateCst,
onDeleteCst,
children
2024-09-19 17:48:48 +03:00
}: React.PropsWithChildren<RSEditStateProps>) => {
2024-06-07 20:17:03 +03:00
const router = useConceptNavigation();
const { user } = useAuth();
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 model = useRSForm();
2025-01-15 23:03:23 +03:00
const isMutable = useMemo(() => role > UserRole.READER && !model.schema?.read_only, [role, model.schema?.read_only]);
2024-06-07 20:17:03 +03:00
const isContentEditable = useMemo(() => isMutable && !model.isArchive, [isMutable, model.isArchive]);
2024-08-01 11:55:45 +03:00
const canDeleteSelected = useMemo(
() => selected.length > 0 && selected.every(id => !model.schema?.cstByID.get(id)?.is_inherited),
[selected, model.schema]
2024-08-01 11:55:45 +03:00
);
const isAttachedToOSS = useMemo(
() =>
!!model.schema &&
model.schema.oss.length > 0 &&
(model.schema.stats.count_inherited > 0 || model.schema.items.length === 0),
[model.schema]
);
2024-06-07 20:17:03 +03:00
const [renameInitialData, setRenameInitialData] = useState<ICstRenameData>();
const showClone = useDialogsStore(state => state.showCloneLibraryItem);
const showCreateVersion = useDialogsStore(state => state.showCreateVersion);
const showEditVersions = useDialogsStore(state => state.showEditVersions);
const showEditEditors = useDialogsStore(state => state.showEditEditors);
const showEditLocation = useDialogsStore(state => state.showChangeLocation);
const showCreateCst = useDialogsStore(state => state.showCreateCst);
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
const showRenameCst = useDialogsStore(state => state.showRenameCst);
const showEditTerm = useDialogsStore(state => state.showEditWordForms);
const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
const showInlineSynthesis = useDialogsStore(state => state.showInlineSynthesis);
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
const showUpload = useDialogsStore(state => state.showUploadRSForm);
const showQR = useDialogsStore(state => state.showQR);
2024-06-07 20:17:03 +03:00
2024-11-20 00:32:26 +03:00
const typeInfo = useMemo(
() =>
model.schema
? model.schema.items.map(item => ({
alias: item.alias,
result: item.parse.typification,
args: item.parse.args
}))
: [],
[model.schema]
);
useEffect(
2024-06-07 20:17:03 +03:00
() =>
2025-01-15 23:03:23 +03:00
adjustRole({
isOwner: model.isOwned,
isEditor: (user && model.schema?.editors.includes(user?.id)) ?? false,
isStaff: user?.is_staff ?? false,
adminMode: adminMode
2024-06-07 20:17:03 +03:00
}),
2025-01-15 23:03:23 +03:00
[model.schema, adjustRole, model.isOwned, user, adminMode]
2024-06-07 20:17:03 +03:00
);
2024-08-01 00:35:49 +03:00
const updateSchema = useCallback(
(data: ILibraryUpdateData) => model.update(data, () => toast.success(information.changesSaved)),
[model]
);
2024-06-07 20:17:03 +03:00
const viewVersion = useCallback(
(version?: VersionID, newTab?: boolean) => router.push(urls.schema(model.itemID, version), newTab),
[router, model]
);
const viewPredecessor = useCallback(
(target: ConstituentaID) =>
model.findPredecessor({ target: target }, reference =>
router.push(
urls.schema_props({
id: reference.schema,
active: reference.id,
tab: RSTabID.CST_EDIT
})
)
),
[router, model]
);
2024-08-01 00:35:49 +03:00
const viewOSS = useCallback(
(target: LibraryItemID, newTab?: boolean) => router.push(urls.oss(target), newTab),
[router]
);
2024-06-07 20:17:03 +03:00
const restoreVersion = useCallback(() => {
if (!model.versionID || !window.confirm(prompts.restoreArchive)) {
return;
}
model.versionRestore(model.versionID, () => {
toast.success(information.versionRestored);
viewVersion(undefined);
});
}, [model, viewVersion]);
const calculateCloneLocation = useCallback(() => {
2024-06-07 20:17:03 +03:00
if (!model.schema) {
return LocationHead.USER;
}
const location = model.schema.location;
const head = model.schema.location.substring(0, 2) as LocationHead;
if (head === LocationHead.LIBRARY) {
return user?.is_staff ? location : LocationHead.USER;
}
if (model.schema.owner === user?.id) {
return location;
}
return head === LocationHead.USER ? LocationHead.USER : location;
}, [model.schema, user]);
2024-06-07 20:17:03 +03:00
const handleCreateCst = useCallback(
(data: ICstCreateData) => {
if (!model.schema) {
return;
}
data.alias = data.alias || generateAlias(data.cst_type, model.schema);
model.cstCreate(data, newCst => {
toast.success(information.newConstituent(newCst.alias));
setSelected([newCst.id]);
if (onCreateCst) onCreateCst(newCst);
});
},
[model, setSelected, onCreateCst]
);
const handleRenameCst = useCallback(
(data: ICstRenameData) => {
const oldAlias = renameInitialData?.alias ?? '';
2024-06-07 20:17:03 +03:00
model.cstRename(data, () => toast.success(information.renameComplete(oldAlias, data.alias)));
},
[model, renameInitialData]
);
const handleSubstituteCst = useCallback(
(data: ICstSubstituteData) => {
2024-09-02 12:10:49 +03:00
model.cstSubstitute(data, () => {
setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)));
toast.success(information.substituteSingle);
});
2024-06-07 20:17:03 +03:00
},
2024-09-02 12:10:49 +03:00
[model, setSelected]
2024-06-07 20:17:03 +03:00
);
const handleDeleteCst = useCallback(
(deleted: ConstituentaID[]) => {
if (!model.schema) {
return;
}
const data = {
items: deleted
};
const deletedNames = deleted.map(id => model.schema!.cstByID.get(id)!.alias).join(', ');
const isEmpty = deleted.length === model.schema.items.length;
const nextActive = isEmpty ? undefined : getNextActiveOnDelete(activeCst?.id, model.schema.items, deleted);
model.cstDelete(data, () => {
toast.success(information.constituentsDestroyed(deletedNames));
setSelected(nextActive ? [nextActive] : []);
2024-12-04 22:52:45 +03:00
onDeleteCst?.(nextActive);
2024-06-07 20:17:03 +03:00
});
},
[model, activeCst, onDeleteCst, setSelected]
);
const handleSaveWordforms = useCallback(
(forms: TermForm[]) => {
if (!activeCst) {
return;
}
const data: ICstUpdateData = {
2024-08-10 11:41:28 +03:00
target: activeCst.id,
item_data: { term_forms: forms }
2024-06-07 20:17:03 +03:00
};
model.cstUpdate(data, () => toast.success(information.changesSaved));
},
[model, activeCst]
);
const handleCreateVersion = useCallback(
(data: IVersionData) => {
if (!model.schema) {
return;
}
2024-08-24 12:29:09 +03:00
model.versionCreate(data, () => {
2024-06-07 20:17:03 +03:00
toast.success(information.newVersion(data.version));
});
},
[model]
2024-06-07 20:17:03 +03:00
);
const handleDeleteVersion = useCallback(
(versionID: VersionID) => {
if (!model.schema) {
return;
}
model.versionDelete(versionID, () => {
toast.success(information.versionDestroyed);
if (String(versionID) === model.versionID) {
viewVersion(undefined);
}
});
},
[model, viewVersion]
);
const handleUpdateVersion = useCallback(
(versionID: VersionID, data: IVersionData) => {
if (!model.schema) {
return;
}
model.versionUpdate(versionID, data, () => toast.success(information.changesSaved));
},
[model]
);
const handleSetLocation = useCallback(
(newLocation: string) => {
if (!model.schema) {
return;
}
model.setLocation(newLocation, () => toast.success(information.moveComplete));
},
[model]
);
const handleInlineSynthesis = useCallback(
(data: IInlineSynthesisData) => {
if (!model.schema) {
return;
}
const oldCount = model.schema.items.length;
model.inlineSynthesis(data, newSchema => {
setSelected([]);
2024-08-06 14:38:10 +03:00
toast.success(information.addedConstituents(newSchema.items.length - oldCount));
2024-06-07 20:17:03 +03:00
});
},
[model, setSelected]
);
const createVersion = useCallback(() => {
if (!model.schema || (isModified && !promptUnsaved())) {
return;
}
showCreateVersion({
versions: model.schema.versions,
onCreate: handleCreateVersion,
selected: selected,
totalCount: model.schema.items.length
});
}, [isModified, model.schema, selected, handleCreateVersion, showCreateVersion]);
const promptEditVersions = useCallback(() => {
if (!model.schema) {
return;
}
showEditVersions({
versions: model.schema.versions,
onDelete: handleDeleteVersion,
onUpdate: handleUpdateVersion
});
}, [model.schema, handleDeleteVersion, handleUpdateVersion, showEditVersions]);
2024-06-07 20:17:03 +03:00
const moveUp = useCallback(() => {
if (!model.schema?.items || selected.length === 0) {
return;
}
const currentIndex = model.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);
const data: ICstMovetoData = {
2024-06-07 20:17:03 +03:00
items: selected,
move_to: target
};
model.cstMoveTo(data);
}, [model, selected]);
const moveDown = useCallback(() => {
if (!model.schema?.items || selected.length === 0) {
return;
}
let count = 0;
const currentIndex = model.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(model.schema.items.length - 1, currentIndex - count + 2);
2024-06-07 20:17:03 +03:00
const data: ICstMovetoData = {
items: selected,
move_to: target
};
model.cstMoveTo(data);
}, [model, selected]);
const createCst = useCallback(
(type: CstType | undefined, skipDialog: boolean, definition?: string) => {
2024-08-29 12:41:59 +03:00
if (!model.schema) {
return;
}
const targetType = type ?? activeCst?.cst_type ?? CstType.BASE;
2024-06-07 20:17:03 +03:00
const data: ICstCreateData = {
insert_after: activeCst?.id ?? null,
2024-08-29 12:41:59 +03:00
cst_type: targetType,
alias: generateAlias(targetType, model.schema),
2024-06-07 20:17:03 +03:00
term_raw: '',
definition_formal: definition ?? '',
definition_raw: '',
convention: '',
term_forms: []
};
if (skipDialog) {
handleCreateCst(data);
} else {
showCreateCst({ schema: model.schema, onCreate: handleCreateCst, initial: data });
2024-06-07 20:17:03 +03:00
}
},
[activeCst, handleCreateCst, model.schema, showCreateCst]
2024-06-07 20:17:03 +03:00
);
const cloneCst = useCallback(() => {
2024-08-29 12:41:59 +03:00
if (!activeCst || !model.schema) {
2024-06-07 20:17:03 +03:00
return;
}
const data: ICstCreateData = {
insert_after: activeCst.id,
cst_type: activeCst.cst_type,
2024-08-29 12:41:59 +03:00
alias: generateAlias(activeCst.cst_type, model.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);
2024-08-29 12:41:59 +03:00
}, [activeCst, handleCreateCst, model.schema]);
2024-06-07 20:17:03 +03:00
const renameCst = useCallback(() => {
if (!activeCst || !model.schema) {
2024-06-07 20:17:03 +03:00
return;
}
const data: ICstRenameData = {
target: activeCst.id,
alias: activeCst.alias,
cst_type: activeCst.cst_type
};
setRenameInitialData(data);
showRenameCst({
schema: model.schema,
initial: data,
allowChangeType: !activeCst.is_inherited,
onRename: handleRenameCst
});
}, [activeCst, model.schema, handleRenameCst, showRenameCst]);
2024-06-07 20:17:03 +03:00
const substitute = useCallback(() => {
if (!model.schema || (isModified && !promptUnsaved())) {
2024-06-07 20:17:03 +03:00
return;
}
showSubstituteCst({ schema: model.schema, onSubstitute: handleSubstituteCst });
}, [isModified, model.schema, handleSubstituteCst, showSubstituteCst]);
2024-06-07 20:17:03 +03:00
const inlineSynthesis = useCallback(() => {
if (!model.schema || (isModified && !promptUnsaved())) {
return;
}
showInlineSynthesis({ receiver: model.schema, onInlineSynthesis: handleInlineSynthesis });
}, [isModified, model.schema, handleInlineSynthesis, showInlineSynthesis]);
const promptDeleteCst = useCallback(() => {
if (!model.schema) {
2024-06-07 20:17:03 +03:00
return;
}
showDeleteCst({ schema: model.schema, selected: selected, onDelete: handleDeleteCst });
}, [model.schema, selected, handleDeleteCst, showDeleteCst]);
2024-06-07 20:17:03 +03:00
const editTermForms = useCallback(() => {
if (!activeCst) {
return;
}
if (isModified && !promptUnsaved()) {
return;
}
showEditTerm({ target: activeCst, onSave: handleSaveWordforms });
}, [isModified, activeCst, handleSaveWordforms, showEditTerm]);
2024-06-07 20:17:03 +03:00
const reindex = useCallback(() => model.resetAliases(() => toast.success(information.reindexComplete)), [model]);
const reorder = useCallback(() => model.restoreOrder(() => toast.success(information.reorderComplete)), [model]);
const canProduceStructure = useMemo(() => {
return (
!!activeCst &&
!!activeCst.parse.typification &&
activeCst.cst_type !== CstType.BASE &&
activeCst.cst_type !== CstType.CONSTANT
);
}, [activeCst]);
const produceStructure = useCallback(() => {
if (!activeCst) {
return;
}
if (isModified && !promptUnsaved()) {
return;
}
const data: ITargetCst = {
target: activeCst.id
};
model.produceStructure(data, cstList => {
toast.success(information.addedConstituents(cstList.length));
if (cstList.length !== 0) {
setSelected(cstList);
}
});
}, [activeCst, setSelected, model, isModified]);
const setEditors = useCallback(
(newEditors: UserID[]) => {
model.setEditors(newEditors, () => toast.success(information.changesSaved));
},
[model]
);
2024-06-07 20:17:03 +03:00
const promptTemplate = useCallback(() => {
if ((isModified && !promptUnsaved()) || !model.schema) {
2024-06-07 20:17:03 +03:00
return;
}
showCstTemplate({ schema: model.schema, onCreate: handleCreateCst, insertAfter: activeCst?.id });
}, [activeCst, isModified, handleCreateCst, model.schema, showCstTemplate]);
2024-06-07 20:17:03 +03:00
const promptClone = useCallback(() => {
if (!model.schema || (isModified && !promptUnsaved())) {
2024-06-07 20:17:03 +03:00
return;
}
showClone({
base: model.schema,
initialLocation: calculateCloneLocation(),
selected: selected,
totalCount: model.schema.items.length
});
}, [isModified, model.schema, selected, showClone, calculateCloneLocation]);
2024-06-07 20:17:03 +03:00
const promptEditors = useCallback(() => {
if (!model.schema) {
return;
}
showEditEditors({ editors: model.schema.editors, setEditors: setEditors });
}, [model.schema, showEditEditors, setEditors]);
2024-06-07 20:17:03 +03:00
const promptLocation = useCallback(() => {
if (!model.schema) {
return;
}
showEditLocation({ initial: model.schema.location, onChangeLocation: handleSetLocation });
}, [model.schema, showEditLocation, handleSetLocation]);
2024-06-07 20:17:03 +03:00
const download = useCallback(() => {
if (isModified && !promptUnsaved()) {
return;
}
const fileName = (model.schema?.alias ?? 'Schema') + EXTEOR_TRS_FILE;
model.download((data: Blob) => {
try {
fileDownload(data, fileName);
} catch (error) {
console.error(error);
}
});
}, [model, isModified]);
const share = useCallback(() => {
const currentRef = window.location.href;
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
navigator.clipboard
.writeText(url)
.then(() => toast.success(information.linkReady))
.catch(console.error);
}, []);
const setOwner = useCallback(
(newOwner: UserID) => {
model.setOwner(newOwner, () => toast.success(information.changesSaved));
},
[model]
);
const setAccessPolicy = useCallback(
(newPolicy: AccessPolicy) => {
model.setAccessPolicy(newPolicy, () => toast.success(information.changesSaved));
},
[model]
);
2024-12-18 12:35:17 +03:00
function generateQR(): string {
const currentRef = window.location.href;
return currentRef.includes('?') ? currentRef + '&qr' : currentRef + '?qr';
}
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={{
schema: model.schema,
2024-08-01 00:35:49 +03:00
updateSchema,
2024-06-07 20:17:03 +03:00
selected,
isMutable,
isContentEditable,
isProcessing: model.processing,
isAttachedToOSS,
2024-06-07 20:17:03 +03:00
canProduceStructure,
2024-08-01 11:55:45 +03:00
canDeleteSelected,
2024-06-07 20:17:03 +03:00
setOwner,
setAccessPolicy,
promptEditors,
promptLocation,
setSelected: setSelected,
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([]),
2024-08-01 00:35:49 +03:00
viewOSS,
2024-06-07 20:17:03 +03:00
viewVersion,
viewPredecessor,
2024-06-07 20:17:03 +03:00
createVersion,
restoreVersion,
promptEditVersions,
2024-06-07 20:17:03 +03:00
moveUp,
moveDown,
createCst,
cloneCst,
renameCst,
promptDeleteCst,
2024-06-07 20:17:03 +03:00
editTermForms,
promptTemplate,
promptClone,
promptUpload: () => showUpload({ upload: model.upload }),
2024-06-07 20:17:03 +03:00
download,
share,
reindex,
reorder,
inlineSynthesis,
produceStructure,
2024-11-20 00:32:26 +03:00
substitute,
showTypeGraph: () => showTypeGraph({ items: typeInfo }),
showQR: () => showQR({ target: generateQR() })
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;
}