import React, { useState, useEffect, useCallback } from 'react'; import { isPopulated, IUser, pagePathUtils, IPagePopulatedToShowRevision, } from '@growi/core'; import { useTranslation } from 'next-i18next'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; import { DropdownItem } from 'reactstrap'; import { exportAsMarkdown, updateContentWidth, useUpdateStateAfterSave } from '~/client/services/page-operation'; import { toastSuccess, toastError } from '~/client/util/apiNotification'; import { apiPost } from '~/client/util/apiv1-client'; import { IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity, } from '~/interfaces/page'; import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui'; import { useCurrentPageId, useCurrentPathname, useIsNotFound, useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId, useTemplateTagData, useIsContainerFluid, useIsIdenticalPath, } from '~/stores/context'; import { usePageTagsForEditors } from '~/stores/editor'; import { usePageAccessoriesModal, PageAccessoriesModalContents, IPageForPageDuplicateModal, usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal, } from '~/stores/modal'; import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page'; import { EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel, useIsAbleToChangeEditorMode, useIsAbleToShowPageAuthors, } from '~/stores/ui'; import CreateTemplateModal from '../CreateTemplateModal'; import AttachmentIcon from '../Icons/AttachmentIcon'; import HistoryIcon from '../Icons/HistoryIcon'; import PresentationIcon from '../Icons/PresentationIcon'; import ShareLinkIcon from '../Icons/ShareLinkIcon'; import { NotAvailable } from '../NotAvailable'; import { NotAvailableForNow } from '../NotAvailableForNow'; import { Skeleton } from '../Skeleton'; import type { AuthorInfoProps } from './AuthorInfo'; import { GrowiSubNavigation } from './GrowiSubNavigation'; import type { SubNavButtonsProps } from './SubNavButtons'; import AuthorInfoStyles from './AuthorInfo.module.scss'; import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss'; const { isUsersHomePage } = pagePathUtils; const AuthorInfoSkeleton = () => ; const PageEditorModeManager = dynamic( () => import('./PageEditorModeManager'), { ssr: false, loading: () => }, ); // TODO: If enable skeleton, we get hydration error when create a page from PageCreateModal // { ssr: false, loading: () => }, const SubNavButtons = dynamic( () => import('./SubNavButtons').then(mod => mod.SubNavButtons), { ssr: false, loading: () => <> }, ); const AuthorInfo = dynamic(() => import('./AuthorInfo').then(mod => mod.AuthorInfo), { ssr: false, loading: AuthorInfoSkeleton, }); type PageOperationMenuItemsProps = { pageId: string, revisionId: string, isLinkSharingDisabled?: boolean, } const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element => { const { t } = useTranslation(); const { pageId, revisionId, isLinkSharingDisabled, } = props; const { data: isGuestUser } = useIsGuestUser(); const { data: isSharedUser } = useIsSharedUser(); const { open: openPresentationModal } = usePagePresentationModal(); const { open: openAccessoriesModal } = usePageAccessoriesModal(); const hrefForPresentationModal = `${pageId}/?presentation=1`; return ( <> {/* Presentation */} openPresentationModal(hrefForPresentationModal)} data-testid="open-presentation-modal-btn" className="grw-page-control-dropdown-item" > { t('Presentation Mode') } {/* Export markdown */} exportAsMarkdown(pageId, revisionId, 'md')} className="grw-page-control-dropdown-item" > {t('export_bulk.export_page_markdown')} {/* TODO: show Tooltip when menu is disabled refs: PageAccessoriesModalControl */} openAccessoriesModal(PageAccessoriesModalContents.PageHistory)} disabled={isGuestUser || isSharedUser} data-testid="open-page-accessories-modal-btn-with-history-tab" className="grw-page-control-dropdown-item" > {t('History')} openAccessoriesModal(PageAccessoriesModalContents.Attachment)} data-testid="open-page-accessories-modal-btn-with-attachment-data-tab" className="grw-page-control-dropdown-item" > {t('attachment_data')} { !isGuestUser && !isSharedUser && ( openAccessoriesModal(PageAccessoriesModalContents.ShareLink)} data-testid="open-page-accessories-modal-btn-with-share-link-management-data-tab" className="grw-page-control-dropdown-item" > {t('share_links.share_link_management')} ) } ); }; type CreateTemplateMenuItemsProps = { onClickTemplateMenuItem: (isPageTemplateModalShown: boolean) => void, } const CreateTemplateMenuItems = (props: CreateTemplateMenuItemsProps): JSX.Element => { const { t } = useTranslation(); const { onClickTemplateMenuItem } = props; const openPageTemplateModalHandler = () => { onClickTemplateMenuItem(true); }; return ( <> {/* Create template */} { t('template.option_label.create/edit') } ); }; type GrowiContextualSubNavigationProps = { currentPage?: IPagePopulatedToShowRevision | null, isCompactMode?: boolean, isLinkSharingDisabled: boolean, }; const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => { const { currentPage } = props; const router = useRouter(); const { data: shareLinkId } = useShareLinkId(); const { mutate: mutateCurrentPage } = useSWRxCurrentPage(); const { data: currentPathname } = useCurrentPathname(); const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? ''); const revision = currentPage?.revision; const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined; const { data: isDrawerMode } = useDrawerMode(); const { data: editorMode, mutate: mutateEditorMode } = useEditorMode(); const { data: pageId } = useCurrentPageId(); const { data: currentUser } = useCurrentUser(); const { data: isNotFound } = useIsNotFound(); const { data: isIdenticalPath } = useIsIdenticalPath(); const { data: isGuestUser } = useIsGuestUser(); const { data: isSharedUser } = useIsSharedUser(); const { data: isContainerFluid } = useIsContainerFluid(); const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement(); const { data: isAbleToShowTagLabel } = useIsAbleToShowTagLabel(); const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode(); const { data: isAbleToShowPageAuthors } = useIsAbleToShowPageAuthors(); const { mutate: mutateSWRTagsInfo, data: tagsInfoData } = useSWRxTagsInfo(currentPage?._id); // eslint-disable-next-line max-len const { data: tagsForEditors, mutate: mutatePageTagsForEditors, sync: syncPageTagsForEditors } = usePageTagsForEditors(!isSharedPage ? currentPage?._id : undefined); const { open: openDuplicateModal } = usePageDuplicateModal(); const { open: openRenameModal } = usePageRenameModal(); const { open: openDeleteModal } = usePageDeleteModal(); const { data: templateTagData } = useTemplateTagData(); const updateStateAfterSave = useUpdateStateAfterSave(pageId); const path = currentPage?.path ?? currentPathname; useEffect(() => { // Run only when tagsInfoData has been updated if (templateTagData == null) { syncPageTagsForEditors(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [tagsInfoData?.tags]); useEffect(() => { if (pageId === null && templateTagData != null) { mutatePageTagsForEditors(templateTagData); } }, [pageId, mutatePageTagsForEditors, templateTagData, mutateSWRTagsInfo]); const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false); const { isCompactMode, isLinkSharingDisabled } = props; const isViewMode = editorMode === EditorMode.View; const tagsUpdatedHandlerForViewMode = useCallback(async(newTags: string[]) => { if (currentPage == null) { return; } const { _id: pageId, revision: revisionId } = currentPage; try { await apiPost('/tags.update', { pageId, revisionId, tags: newTags }); updateStateAfterSave?.(); toastSuccess('updated tags successfully'); } catch (err) { toastError(err, 'fail to update tags'); } }, [currentPage, updateStateAfterSave]); const tagsUpdatedHandlerForEditMode = useCallback((newTags: string[]): void => { // It will not be reflected in the DB until the page is refreshed mutatePageTagsForEditors(newTags); return; }, [mutatePageTagsForEditors]); const reload = useCallback(() => { if (currentPathname != null) { router.push(currentPathname); } }, [currentPathname, router]); const duplicateItemClickedHandler = useCallback(async(page: IPageForPageDuplicateModal) => { const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => { router.push(toPath); }; openDuplicateModal(page, { onDuplicated: duplicatedHandler }); }, [openDuplicateModal, router]); const renameItemClickedHandler = useCallback(async(page: IPageToRenameWithMeta) => { const renamedHandler: OnRenamedFunction = () => { reload(); }; openRenameModal(page, { onRenamed: renamedHandler }); }, [openRenameModal, reload]); const deleteItemClickedHandler = useCallback((pageWithMeta: IPageWithMeta) => { const deletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => { if (typeof pathOrPathsToDelete !== 'string') { return; } const path = pathOrPathsToDelete; if (isCompletely) { // redirect to NotFound Page router.push(path); } else if (currentPathname != null) { router.push(currentPathname); } }; openDeleteModal([pageWithMeta], { onDeleted: deletedHandler }); }, [currentPathname, openDeleteModal, router]); const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => { if (!isSharedPage) { await updateContentWidth(pageId, value); mutateCurrentPage(); } }, [isSharedPage, mutateCurrentPage]); const templateMenuItemClickHandler = useCallback(() => { setIsPageTempleteModalShown(true); }, []); const RightComponent = () => { const additionalMenuItemsRenderer = () => { if (revisionId == null || pageId == null) { return ( <> ); } return ( <> ); }; return ( <>
{ isViewMode && (
{ pageId != null && ( ) }
) } {isAbleToChangeEditorMode && ( mutateEditorMode(viewType)} isBtnDisabled={isGuestUser} editorMode={editorMode} /> )}
{ (isAbleToShowPageAuthors && !isCompactMode && !isUsersHomePage(path ?? '')) && (
  • { currentPage != null ? : }
  • { currentPage != null ? : }
) }
{path != null && currentUser != null && ( setIsPageTempleteModalShown(false)} /> )} ); }; const pagePath = isIdenticalPath || isNotFound ? currentPathname : currentPage?.path; return ( ); }; export default GrowiContextualSubNavigation;