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 { useAtomValue } from 'jotai'; 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 { usePrintMode } from '~/client/services/use-print-mode'; import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr'; import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar'; import { usePageBulkExportSelectModalActions } from '~/features/page-bulk-export/client/states/modal'; import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui'; import { useShouldExpandContent } from '~/services/layout/use-should-expand-content'; import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/states/context'; import { useCurrentPathname, useCurrentUser } from '~/states/global'; import { useCurrentPageId, useFetchCurrentPage } from '~/states/page'; import { useShareLinkId } from '~/states/page/hooks'; import { disableLinkSharingAtom, isBulkExportPagesEnabledAtom, isLocalAccountRegistrationEnabledAtom, isUploadEnabledAtom, } from '~/states/server-configurations'; import { useDeviceLargerThanMd } from '~/states/ui/device'; import { useEditorMode } from '~/states/ui/editor'; import { PageAccessoriesModalContents, usePageAccessoriesModalActions } from '~/states/ui/modal/page-accessories'; import { usePageDeleteModalActions } from '~/states/ui/modal/page-delete'; import { usePageDuplicateModalActions, type IPageForPageDuplicateModal } from '~/states/ui/modal/page-duplicate'; import { usePresentationModalActions } from '~/states/ui/modal/page-presentation'; import { usePageRenameModalActions } from '~/states/ui/modal/page-rename'; import { useIsAbleToShowPageManagement, useIsAbleToChangeEditorMode, } from '~/states/ui/page-abilities'; import { useSWRxPageInfo, } from '~/stores/page'; import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing'; import { CreateTemplateModalLazyLoaded } from '../CreateTemplateModal'; import { NotAvailable } from '../NotAvailable'; import { Skeleton } from '../Skeleton'; import styles from './GrowiContextualSubNavigation.module.scss'; import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss'; const moduleClass = styles['grw-contextual-sub-navigation']; const minHeightSubNavigation = styles['grw-min-height-sub-navigation']; 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 isGuestUser = useIsGuestUser(); const isReadOnlyUser = useIsReadOnlyUser(); const isSharedUser = useIsSharedUser(); const isBulkExportPagesEnabled = useAtomValue(isBulkExportPagesEnabledAtom); const isUploadEnabled = useAtomValue(isUploadEnabledAtom); const { open: openPresentationModal } = usePresentationModalActions(); const { open: openAccessoriesModal } = usePageAccessoriesModalActions(); const { open: openPageBulkExportSelectModal } = usePageBulkExportSelectModalActions(); 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, }; const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => { const { currentPage } = props; const { t } = useTranslation(); const router = useRouter(); const isPrinting = usePrintMode(); const shareLinkId = useShareLinkId(); const { fetchCurrentPage } = useFetchCurrentPage(); const currentPathname = useCurrentPathname(); const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? ''); const revision = currentPage?.revision; const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined; const { editorMode } = useEditorMode(); const pageId = useCurrentPageId(true); const currentUser = useCurrentUser(); const isGuestUser = useIsGuestUser(); const isReadOnlyUser = useIsReadOnlyUser(); const isLocalAccountRegistrationEnabled = useAtomValue(isLocalAccountRegistrationEnabledAtom); const isLinkSharingDisabled = useAtomValue(disableLinkSharingAtom); const isSharedUser = useIsSharedUser(); const shouldExpandContent = useShouldExpandContent(currentPage); const isAbleToShowPageManagement = useIsAbleToShowPageManagement(); const isAbleToChangeEditorMode = useIsAbleToChangeEditorMode(); const [isDeviceLargerThanMd] = useDeviceLargerThanMd(); const { open: openDuplicateModal } = usePageDuplicateModalActions(); const { open: openRenameModal } = usePageRenameModalActions(); const { open: openDeleteModal } = usePageDeleteModalActions(); const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId); const [isStickyActive, setStickyActive] = useState(false); const path = currentPage?.path ?? currentPathname; const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false); 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 = () => { fetchCurrentPage({ force: true }); mutatePageInfo(); mutatePageTree(); mutateRecentlyUpdated(); }; openRenameModal(page, { onRenamed: renamedHandler }); }, [fetchCurrentPage, 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); } fetchCurrentPage({ force: true }); mutatePageInfo(); mutatePageTree(); mutateRecentlyUpdated(); }; openDeleteModal([pageWithMeta], { onDeleted: deletedHandler }); }, [currentPathname, fetchCurrentPage, openDeleteModal, router, mutatePageInfo]); const switchContentWidthHandler = useCallback(async (pageId: string, value: boolean) => { if (!isSharedPage) { await updateContentWidth(pageId, value); fetchCurrentPage({ force: true }); } }, [isSharedPage, fetchCurrentPage]); const additionalMenuItemsRenderer = useCallback(() => { if (revisionId == null || pageId == null) { return ( <> {!isReadOnlyUser && ( setIsPageTempleteModalShown(true)} /> ) } > ); } return ( <> {!isReadOnlyUser && ( <> setIsPageTempleteModalShown(true)} /> > ) } > ); }, [isLinkSharingDisabled, pageId, revisionId, isReadOnlyUser]); // 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" > {isAbleToChangeEditorMode && ( )} {isGuestUser && ( person_add{t('Sign up')} {!isLocalAccountRegistrationEnabled && ( {t('tooltip.login_required')} )} login{t('Sign in')} )} {path != null && currentUser != null && !isReadOnlyUser && ( setIsPageTempleteModalShown(false)} /> )} > ); }; export default GrowiContextualSubNavigation;