import type { FC } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import type { IPageInfoForEntity, IPageToDeleteWithMeta } from '@growi/core'; import { isIPageInfoForEntity } from '@growi/core'; import { pagePathUtils } from '@growi/core/dist/utils'; import { useTranslation } from 'next-i18next'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import { apiPost } from '~/client/util/apiv1-client'; import { apiv3Post } from '~/client/util/apiv3-client'; import type { IDeleteManyPageApiv3Result, IDeleteSinglePageApiv1Result, } from '~/interfaces/page'; import { usePageDeleteModalActions, usePageDeleteModalStatus, } from '~/states/ui/modal/page-delete'; import { useSWRxPageInfoForList } from '~/stores/page-listing'; import loggerFactory from '~/utils/logger'; import ApiErrorMessageList from '../PageManagement/ApiErrorMessageList'; const { isTrashPage } = pagePathUtils; const logger = loggerFactory('growi:cli:PageDeleteModal'); const deleteIconAndKey = { completely: { color: 'danger', icon: 'delete_forever', translationKey: 'completely', }, temporary: { color: 'warning', icon: 'delete', translationKey: 'page', }, }; const isIPageInfoForEntityForDeleteModal = ( pageInfo: any | undefined, ): pageInfo is IPageInfoForEntity => { return ( pageInfo != null && 'isDeletable' in pageInfo && 'isAbleToDeleteCompletely' in pageInfo ); }; export const PageDeleteModal: FC = () => { const { t } = useTranslation(); const { isOpened, pages, opts } = usePageDeleteModalStatus() ?? {}; const { close: closeDeleteModal } = usePageDeleteModalActions(); // Optimize deps: use page IDs and length instead of pages array reference const pageIds = useMemo(() => pages?.map((p) => p.data._id) ?? [], [pages]); const pagesLength = pages?.length ?? 0; // biome-ignore lint/correctness/useExhaustiveDependencies: keep optimized deps const notOperatablePages: IPageToDeleteWithMeta[] = useMemo( () => (pages ?? []).filter((p) => !isIPageInfoForEntityForDeleteModal(p.meta)), // Optimization: Use pageIds and pagesLength instead of pages array reference to avoid unnecessary re-computation [pageIds, pagesLength], ); const notOperatablePageIds = useMemo( () => notOperatablePages.map((p) => p.data._id), [notOperatablePages], ); const { injectTo } = useSWRxPageInfoForList(notOperatablePageIds); // inject IPageInfo to operate // biome-ignore lint/correctness/useExhaustiveDependencies: keep optimized deps const injectedPages = useMemo( () => { if (pages != null) { return injectTo(pages); } return null; }, // Optimization: Use pageIds and pagesLength instead of pages array reference to avoid unnecessary re-computation [pageIds, pagesLength, injectTo], ); // calculate conditions to delete const [isDeletable, isAbleToDeleteCompletely] = useMemo(() => { if (injectedPages != null && injectedPages.length > 0) { const isDeletable = injectedPages.every( (pageWithMeta) => pageWithMeta.meta?.isDeletable, ); const isAbleToDeleteCompletely = injectedPages.every( (pageWithMeta) => pageWithMeta.meta?.isAbleToDeleteCompletely, ); return [isDeletable, isAbleToDeleteCompletely]; } return [true, true]; }, [injectedPages]); // Optimize deps: use page paths for trash detection // biome-ignore lint/correctness/useExhaustiveDependencies: keep optimized deps const pagePaths = useMemo( () => pages?.map((p) => p.data?.path ?? '') ?? [], // Optimization: Use pageIds and pagesLength instead of pages array reference to avoid unnecessary re-computation [pageIds, pagesLength], ); // calculate condition to determine modal status const forceDeleteCompletelyMode = useMemo(() => { if (pagesLength > 0) { return pagePaths.every((path) => isTrashPage(path)); } return false; }, [pagePaths, pagesLength]); const [isDeleteRecursively, setIsDeleteRecursively] = useState(true); const [isDeleteCompletely, setIsDeleteCompletely] = useState( forceDeleteCompletelyMode, ); const deleteMode = forceDeleteCompletelyMode || isDeleteCompletely ? 'completely' : 'temporary'; const [errs, setErrs] = useState(null); // initialize when opening modal useEffect(() => { if (isOpened) { setIsDeleteRecursively(true); setIsDeleteCompletely(forceDeleteCompletelyMode); } }, [forceDeleteCompletelyMode, isOpened]); useEffect(() => { setIsDeleteCompletely(forceDeleteCompletelyMode); }, [forceDeleteCompletelyMode]); const changeIsDeleteRecursivelyHandler = useCallback(() => { setIsDeleteRecursively(!isDeleteRecursively); }, [isDeleteRecursively]); const changeIsDeleteCompletelyHandler = useCallback(() => { if (forceDeleteCompletelyMode) { return; } setIsDeleteCompletely(!isDeleteCompletely); }, [forceDeleteCompletelyMode, isDeleteCompletely]); // biome-ignore lint/correctness/useExhaustiveDependencies: keep optimized deps const deletePage = useCallback( async () => { if (pages == null) { return; } if (!isDeletable) { logger.error('At least one page is not deletable.'); return; } /* * When multiple pages */ if (pages.length > 1) { try { const isRecursively = isDeleteRecursively === true ? true : undefined; const isCompletely = isDeleteCompletely === true ? true : undefined; const pageIdToRevisionIdMap = {}; pages.forEach((p) => { pageIdToRevisionIdMap[p.data._id] = p.data.revision as string; }); const { data } = await apiv3Post( '/pages/delete', { pageIdToRevisionIdMap, isRecursively, isCompletely, }, ); const onDeleted = opts?.onDeleted; if (onDeleted != null) { onDeleted(data.paths, data.isRecursively, data.isCompletely); } closeDeleteModal(); } catch (err) { setErrs([err]); } } else { /* * When single page */ try { const recursively = isDeleteRecursively === true ? true : undefined; const completely = forceDeleteCompletelyMode || isDeleteCompletely ? true : undefined; const page = pages[0].data; const { path, isRecursively, isCompletely } = (await apiPost( '/pages.remove', { page_id: page._id, revision_id: page.revision, recursively, completely, }, )) as IDeleteSinglePageApiv1Result; const onDeleted = opts?.onDeleted; if (onDeleted != null) { onDeleted(path, isRecursively, isCompletely); } closeDeleteModal(); } catch (err) { setErrs([err]); } } }, // Optimization: Use pageIds and pagesLength instead of pages array reference to avoid unnecessary re-computation [ pageIds, pagesLength, isDeletable, isDeleteRecursively, isDeleteCompletely, forceDeleteCompletelyMode, opts?.onDeleted, closeDeleteModal, ], ); const deleteButtonHandler = useCallback(async () => { await deletePage(); }, [deletePage]); const renderDeleteRecursivelyForm = useCallback(() => { return (
); }, [isDeleteRecursively, changeIsDeleteRecursivelyHandler, t]); const renderDeleteCompletelyForm = useCallback(() => { return (
{!isAbleToDeleteCompletely && (

block {t('modal_delete.delete_completely_restriction')}

)}
); }, [ isAbleToDeleteCompletely, isDeleteCompletely, changeIsDeleteCompletelyHandler, t, ]); const headerContent = useMemo(() => { if (!isOpened) { return <>; } return ( {deleteIconAndKey[deleteMode].icon} {t( `modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`, )} ); }, [isOpened, deleteMode, t]); // biome-ignore lint/correctness/useExhaustiveDependencies: keep optimized deps const bodyContent = useMemo(() => { if (!isOpened) { return <>; } // Render page paths to delete inline for better performance const renderingPages = injectedPages != null && injectedPages.length > 0 ? injectedPages : pages; const pagePathsElements = renderingPages != null ? ( renderingPages.map((page) => (

{page.data.path} {isIPageInfoForEntity(page.meta) && !page.meta.isDeletable && ( (CAN NOT TO DELETE) )}

)) ) : ( <> ); return ( <>
{t('modal_delete.deleting_page')}:
{/* Todo: change the way to show path on modal when too many pages are selected */} {pagePathsElements}
{isDeletable && renderDeleteRecursivelyForm()} {isDeletable && !forceDeleteCompletelyMode && renderDeleteCompletelyForm()} ); // Optimization: Use direct dependencies instead of JSX.Element reference for better performance }, [ isOpened, t, pageIds, pagesLength, injectedPages, isDeletable, renderDeleteRecursivelyForm, forceDeleteCompletelyMode, renderDeleteCompletelyForm, ]); const footerContent = useMemo(() => { if (!isOpened) { return <>; } return ( <> ); }, [isOpened, errs, deleteMode, isDeletable, deleteButtonHandler, t]); return ( {headerContent} {bodyContent} {footerContent} ); };