import React, { useState, useEffect, useCallback, useMemo, } from 'react'; import { pagePathUtils } from '@growi/core'; import { useTranslation } from 'next-i18next'; import { Collapse, Modal, ModalHeader, ModalBody, ModalFooter, } from 'reactstrap'; import { debounce } from 'throttle-debounce'; import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client'; import { toastError } from '~/client/util/toastr'; import { isIPageInfoForEntity } from '~/interfaces/page'; import { useSiteUrl, useIsSearchServiceReachable } from '~/stores/context'; import { usePageRenameModal } from '~/stores/modal'; import { useSWRxPageInfo } from '~/stores/page'; import DuplicatedPathsTable from './DuplicatedPathsTable'; import ApiErrorMessageList from './PageManagement/ApiErrorMessageList'; import PagePathAutoComplete from './PagePathAutoComplete'; const isV5Compatible = (meta: unknown): boolean => { return isIPageInfoForEntity(meta) ? meta.isV5Compatible : true; }; const PageRenameModal = (): JSX.Element => { const { t } = useTranslation(); const { isUsersHomePage } = pagePathUtils; const { data: siteUrl } = useSiteUrl(); const { data: renameModalData, close: closeRenameModal } = usePageRenameModal(); const { data: isReachable } = useIsSearchServiceReachable(); const isOpened = renameModalData?.isOpened ?? false; const page = renameModalData?.page; const shouldFetch = isOpened && page != null && !isIPageInfoForEntity(page.meta); const { data: pageInfo } = useSWRxPageInfo(shouldFetch ? page?.data._id : null); if (page != null && pageInfo != null) { page.meta = pageInfo; } const [pageNameInput, setPageNameInput] = useState(''); const [errs, setErrs] = useState(null); const [subordinatedPages, setSubordinatedPages] = useState([]); const [existingPaths, setExistingPaths] = useState([]); const [isRenameRecursively, setIsRenameRecursively] = useState(true); const [isRenameRedirect, setIsRenameRedirect] = useState(false); const [isRemainMetadata, setIsRemainMetadata] = useState(false); const [expandOtherOptions, setExpandOtherOptions] = useState(false); const [subordinatedError] = useState(null); const [isMatchedWithUserHomePagePath, setIsMatchedWithUserHomePagePath] = useState(false); const updateSubordinatedList = useCallback(async() => { if (page == null) { return; } const { path } = page.data; try { const res = await apiv3Get('/pages/subordinated-list', { path }); setSubordinatedPages(res.data.subordinatedPages); } catch (err) { setErrs(err); toastError(t('modal_rename.label.Failed to get subordinated pages')); } }, [page, t]); useEffect(() => { if (page != null && isOpened) { updateSubordinatedList(); setPageNameInput(page.data.path); } }, [isOpened, page, updateSubordinatedList]); const canRename = useMemo(() => { if (page == null || isMatchedWithUserHomePagePath || page.data.path === pageNameInput) { return false; } if (isV5Compatible(page.meta)) { return existingPaths.length === 0; // v5 data } return isRenameRecursively; // v4 data }, [existingPaths.length, isMatchedWithUserHomePagePath, isRenameRecursively, page, pageNameInput]); const rename = useCallback(async() => { if (page == null || !canRename) { return; } const _isV5Compatible = isV5Compatible(page.meta); setErrs(null); const { _id, path, revision } = page.data; try { const response = await apiv3Put('/pages/rename', { pageId: _id, revisionId: revision ?? null, isRecursively: !_isV5Compatible ? isRenameRecursively : undefined, isRenameRedirect, updateMetadata: !isRemainMetadata, newPagePath: pageNameInput, path, }); const { page } = response.data; const url = new URL(page.path, 'https://dummy'); if (isRenameRedirect) { url.searchParams.append('withRedirect', 'true'); } const onRenamed = renameModalData?.opts?.onRenamed; if (onRenamed != null) { onRenamed(path); } closeRenameModal(); } catch (err) { setErrs(err); } }, [closeRenameModal, canRename, isRemainMetadata, isRenameRecursively, isRenameRedirect, page, pageNameInput, renameModalData?.opts?.onRenamed]); const checkExistPaths = useCallback(async(fromPath, toPath) => { if (page == null) { return; } try { const res = await apiv3Get<{ existPaths: string[]}>('/page/exist-paths', { fromPath, toPath }); const { existPaths } = res.data; setExistingPaths(existPaths); } catch (err) { // Do not toast in case of this error because debounce process may be executed after the renaming process is completed. if (err.length === 1 && err[0].code === 'from-page-is-not-exist') { return; } setErrs(err); toastError(t('modal_rename.label.Failed to get exist path')); } }, [page, t]); const checkExistPathsDebounce = useMemo(() => { return debounce(1000, checkExistPaths); }, [checkExistPaths]); const checkIsUsersHomePageDebounce = useMemo(() => { const checkIsPagePathRenameable = () => { setIsMatchedWithUserHomePagePath(isUsersHomePage(pageNameInput)); }; return debounce(1000, checkIsPagePathRenameable); }, [isUsersHomePage, pageNameInput]); useEffect(() => { if (isOpened && page != null && pageNameInput !== page.data.path) { checkExistPathsDebounce(page.data.path, pageNameInput); checkIsUsersHomePageDebounce(pageNameInput); } }, [isOpened, pageNameInput, subordinatedPages, checkExistPathsDebounce, page, checkIsUsersHomePageDebounce]); function ppacInputChangeHandler(value) { setErrs(null); setPageNameInput(value); } /** * change pageNameInput * @param {string} value */ function inputChangeHandler(value) { setErrs(null); setPageNameInput(value); } useEffect(() => { if (isOpened || page == null) { return; } // reset states after the modal closed setTimeout(() => { setPageNameInput(''); setErrs(null); setSubordinatedPages([]); setExistingPaths([]); setIsRenameRecursively(true); setIsRenameRedirect(false); setIsRemainMetadata(false); setExpandOtherOptions(false); }, 1000); }, [isOpened, page]); const bodyContent = () => { if (!isOpened || page == null) { return <>; } const { path } = page.data; const isTargetPageDuplicate = existingPaths.includes(pageNameInput); return ( <>

{ path }

{siteUrl}
{ e.preventDefault(); rename() }}> {isReachable ? ( ) : ( inputChangeHandler(e.target.value)} required autoFocus /> )}
{ isTargetPageDuplicate && (

Error: Target path is duplicated.

) } { isMatchedWithUserHomePagePath && (

Error: Cannot move to directory under /user page.

) } { !isV5Compatible(page.meta) && ( <>
setIsRenameRecursively(!isRenameRecursively)} />
setIsRenameRecursively(!isRenameRecursively)} /> {isRenameRecursively && existingPaths.length !== 0 && ( ) }
) }

setIsRenameRedirect(!isRenameRedirect)} />
setIsRemainMetadata(!isRemainMetadata)} />
{subordinatedError}
); }; const footerContent = () => { if (!isOpened || page == null) { return <>; } const submitButtonDisabled = !canRename; return ( <> ); }; return ( { t('modal_rename.label.Move/Rename page') } {bodyContent()} {footerContent()} ); }; export default PageRenameModal;