import React, { useState, useCallback, useMemo, type JSX, } from 'react'; import { isPopulated } from '@growi/core'; import type { IPagePopulatedToShowRevision, IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity, } from '@growi/core'; import { pagePathUtils } from '@growi/core/dist/utils'; import { GlobalCodeMirrorEditorKey } from '@growi/editor'; import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor'; import { useTranslation } from 'next-i18next'; import dynamic from 'next/dynamic'; import Link from 'next/link'; import { useRouter } from 'next/router'; import Sticky from 'react-stickynode'; import { DropdownItem, UncontrolledTooltip, Tooltip } from 'reactstrap'; import { exportAsMarkdown, updateContentWidth, syncLatestRevisionBody } from '~/client/services/page-operation'; import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr'; import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar'; import { usePageBulkExportSelectModal } from '~/features/page-bulk-export/client/stores/modal'; import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui'; import { useShouldExpandContent } from '~/services/layout/use-should-expand-content'; import { useCurrentPathname, useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsBulkExportPagesEnabled, useIsLocalAccountRegistrationEnabled, useIsSharedUser, useShareLinkId, useIsUploadEnabled, } from '~/stores-universal/context'; import { useEditorMode } from '~/stores-universal/ui'; import { usePageAccessoriesModal, PageAccessoriesModalContents, type IPageForPageDuplicateModal, usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal, } from '~/stores/modal'; import { useSWRMUTxCurrentPage, useCurrentPageId, useSWRxPageInfo, } from '~/stores/page'; import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing'; import { useIsAbleToShowPageManagement, useIsAbleToChangeEditorMode, useIsDeviceLargerThanMd, } from '~/stores/ui'; import { NotAvailable } from '../NotAvailable'; import { Skeleton } from '../Skeleton'; import styles from './GrowiContextualSubNavigation.module.scss'; import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss'; const CreateTemplateModal = dynamic(() => import('../CreateTemplateModal').then(mod => mod.CreateTemplateModal), { ssr: false }); const PageEditorModeManager = dynamic( () => import('./PageEditorModeManager').then(mod => mod.PageEditorModeManager), { ssr: false, loading: () => }, ); const PageControls = dynamic( () => import('../PageControls').then(mod => mod.PageControls), { ssr: false, loading: () => <> }, ); 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: isReadOnlyUser } = useIsReadOnlyUser(); const { data: isSharedUser } = useIsSharedUser(); const { data: isBulkExportPagesEnabled } = useIsBulkExportPagesEnabled(); const { data: isUploadEnabled } = useIsUploadEnabled(); const { open: openPresentationModal } = usePagePresentationModal(); const { open: openAccessoriesModal } = usePageAccessoriesModal(); const { open: openPageBulkExportSelectModal } = usePageBulkExportSelectModal(); const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN); const [isBulkExportTooltipOpen, setIsBulkExportTooltipOpen] = useState(false); const syncLatestRevisionBodyHandler = useCallback(async() => { // eslint-disable-next-line no-alert const answer = window.confirm(t('sync-latest-revision-body.confirm')); if (answer) { try { const editingMarkdownLength = codeMirrorEditor?.getDoc().length; const res = await syncLatestRevisionBody(pageId, editingMarkdownLength); if (!res.synced) { toastWarning(t('sync-latest-revision-body.skipped-toaster')); return; } if (res?.isYjsDataBroken) { // eslint-disable-next-line no-alert window.alert(t('sync-latest-revision-body.alert')); return; } toastSuccess(t('sync-latest-revision-body.success-toaster')); } catch { toastError(t('sync-latest-revision-body.error-toaster')); } } }, [codeMirrorEditor, pageId, t]); return ( <> syncLatestRevisionBodyHandler()} className="grw-page-control-dropdown-item" > sync {t('sync-latest-revision-body.menuitem')} {/* Presentation */} openPresentationModal()} data-testid="open-presentation-modal-btn" className="grw-page-control-dropdown-item" > jamboard_kiosk {t('Presentation Mode')} {/* Export markdown */} exportAsMarkdown(pageId, revisionId, 'md')} className="grw-page-control-dropdown-item" > cloud_download {t('page_export.export_page_markdown')} {/* Bulk export */} {isBulkExportPagesEnabled && ( <> cloud_download {t('page_export.bulk_export')} setIsBulkExportTooltipOpen(!isBulkExportTooltipOpen)} > {t('page_export.file_upload_not_configured')} )} {/* 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" > history {t('History')} openAccessoriesModal(PageAccessoriesModalContents.Attachment)} data-testid="open-page-accessories-modal-btn-with-attachment-data-tab" className="grw-page-control-dropdown-item" > attachment {t('attachment_data')} {!isGuestUser && !isReadOnlyUser && !isSharedUser && ( openAccessoriesModal(PageAccessoriesModalContents.ShareLink)} data-testid="open-page-accessories-modal-btn-with-share-link-management-data-tab" className="grw-page-control-dropdown-item" > share {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 */} contract_edit {t('template.option_label.create/edit')} ); }; type GrowiContextualSubNavigationProps = { currentPage?: IPagePopulatedToShowRevision | null, isLinkSharingDisabled?: boolean, }; const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => { const { currentPage } = props; const { t } = useTranslation(); const router = useRouter(); const { data: shareLinkId } = useShareLinkId(); const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage(); const { data: currentPathname } = useCurrentPathname(); const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? ''); const revision = currentPage?.revision; const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined; const { data: editorMode } = useEditorMode(); const { data: pageId } = useCurrentPageId(); const { data: currentUser } = useCurrentUser(); const { data: isGuestUser } = useIsGuestUser(); const { data: isReadOnlyUser } = useIsReadOnlyUser(); const { data: isLocalAccountRegistrationEnabled } = useIsLocalAccountRegistrationEnabled(); const { data: isSharedUser } = useIsSharedUser(); const shouldExpandContent = useShouldExpandContent(currentPage); const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement(); const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode(); const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd(); const { open: openDuplicateModal } = usePageDuplicateModal(); const { open: openRenameModal } = usePageRenameModal(); const { open: openDeleteModal } = usePageDeleteModal(); const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId); const [isStickyActive, setStickyActive] = useState(false); const path = currentPage?.path ?? currentPathname; // const grant = currentPage?.grant ?? grantData?.grant; // const grantUserGroupId = currentPage?.grantedGroup?._id ?? grantData?.grantedGroup?.id; const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false); const { isLinkSharingDisabled } = props; 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 = () => { mutateCurrentPage(); mutatePageInfo(); mutatePageTree(); mutateRecentlyUpdated(); }; openRenameModal(page, { onRenamed: renamedHandler }); }, [mutateCurrentPage, mutatePageInfo, openRenameModal]); 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); } mutateCurrentPage(); mutatePageInfo(); mutatePageTree(); mutateRecentlyUpdated(); }; openDeleteModal([pageWithMeta], { onDeleted: deletedHandler }); }, [currentPathname, mutateCurrentPage, openDeleteModal, router, mutatePageInfo]); const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => { if (!isSharedPage) { await updateContentWidth(pageId, value); mutateCurrentPage(); } }, [isSharedPage, mutateCurrentPage]); const additionalMenuItemsRenderer = useCallback(() => { if (revisionId == null || pageId == null) { return ( <> {!isReadOnlyUser && ( setIsPageTempleteModalShown(true)} /> ) } ); } return ( <> {!isReadOnlyUser && ( <> setIsPageTempleteModalShown(true)} /> ) } ); }, [isLinkSharingDisabled, isReadOnlyUser, pageId, revisionId]); // hide sub controls when sticky on mobile device const hideSubControls = useMemo(() => { return !isDeviceLargerThanMd && isStickyActive; }, [isDeviceLargerThanMd, isStickyActive]); return ( <> setStickyActive(status.status === Sticky.STATUS_FIXED)} innerActiveClass="w-100 end-0" > {path != null && currentUser != null && !isReadOnlyUser && ( setIsPageTempleteModalShown(false)} /> )} ); }; export default GrowiContextualSubNavigation;