import { ActionIcon, Group, Menu, Text, Tooltip } from "@mantine/core"; import { IconArrowRight, IconArrowsHorizontal, IconDots, IconFileExport, IconHistory, IconLink, IconList, IconMarkdown, IconMessage, IconPrinter, IconTrash, IconWifiOff, } from "@tabler/icons-react"; import React, { useEffect, useRef, useState } from "react"; import useToggleAside from "@/hooks/use-toggle-aside.tsx"; import { useAtom, useAtomValue } from "jotai"; import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts"; import { useDisclosure, useHotkeys } from "@mantine/hooks"; import { useClipboard } from "@/hooks/use-clipboard"; import { useParams } from "react-router-dom"; import { usePageQuery } from "@/features/page/queries/page-query.ts"; import { buildPageUrl } from "@/features/page/page.utils.ts"; import { notifications } from "@mantine/notifications"; import { getAppUrl } from "@/lib/config.ts"; import { extractPageSlugId } from "@/lib"; import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts"; import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx"; import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx"; import { Trans, useTranslation } from "react-i18next"; import ExportModal from "@/components/common/export-modal"; import { htmlToMarkdown } from "@docmost/editor-ext"; import { pageEditorAtom, yjsConnectionStatusAtom, } from "@/features/editor/atoms/editor-atoms.ts"; import { formattedDate } from "@/lib/time.ts"; import { PageStateSegmentedControl } from "@/features/user/components/page-state-pref.tsx"; import MovePageModal from "@/features/page/components/move-page-modal.tsx"; import { useTimeAgo } from "@/hooks/use-time-ago.tsx"; import ShareModal from "@/features/share/components/share-modal.tsx"; interface PageHeaderMenuProps { readOnly?: boolean; } export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { const { t } = useTranslation(); const toggleAside = useToggleAside(); useHotkeys( [ [ "mod+F", () => { const event = new CustomEvent("openFindDialogFromEditor", {}); document.dispatchEvent(event); }, ], [ "Escape", () => { const event = new CustomEvent("closeFindDialogFromEditor", {}); document.dispatchEvent(event); }, { preventDefault: false }, ], ], [], ); return ( <> {!readOnly && } toggleAside("comments")} > toggleAside("toc")} > ); } interface PageActionMenuProps { readOnly?: boolean; } function PageActionMenu({ readOnly }: PageActionMenuProps) { const { t } = useTranslation(); const [, setHistoryModalOpen] = useAtom(historyAtoms); const clipboard = useClipboard({ timeout: 500 }); const { pageSlug, spaceSlug } = useParams(); const { data: page, isLoading } = usePageQuery({ pageId: extractPageSlugId(pageSlug), }); const { openDeleteModal } = useDeletePageModal(); const [tree] = useAtom(treeApiAtom); const [exportOpened, { open: openExportModal, close: closeExportModal }] = useDisclosure(false); const [ movePageModalOpened, { open: openMovePageModal, close: closeMoveSpaceModal }, ] = useDisclosure(false); const [pageEditor] = useAtom(pageEditorAtom); const pageUpdatedAt = useTimeAgo(page?.updatedAt); const handleCopyLink = () => { const pageUrl = getAppUrl() + buildPageUrl(spaceSlug, page.slugId, page.title); clipboard.copy(pageUrl); notifications.show({ message: t("Link copied") }); }; const handleCopyAsMarkdown = () => { if (!pageEditor) return; const html = pageEditor.getHTML(); const markdown = htmlToMarkdown(html); const title = page?.title ? `# ${page.title}\n\n` : ""; clipboard.copy(`${title}${markdown}`); notifications.show({ message: t("Copied") }); }; const handlePrint = () => { setTimeout(() => { window.print(); }, 250); }; const openHistoryModal = () => { setHistoryModalOpen(true); }; const handleDeletePage = () => { openDeleteModal({ onConfirm: () => tree?.delete(page.id) }); }; return ( <> } onClick={handleCopyLink} > {t("Copy link")} } onClick={handleCopyAsMarkdown} > {t("Copy as Markdown")} }> } onClick={openHistoryModal} > {t("Page history")} {!readOnly && ( } onClick={openMovePageModal} > {t("Move")} )} } onClick={openExportModal} > {t("Export")} } onClick={handlePrint} > {t("Print PDF")} {!readOnly && ( <> } onClick={handleDeletePage} > {t("Move to trash")} )} <>
{t("Word count: {{wordCount}}", { wordCount: pageEditor?.storage?.characterCount?.words(), })} }} /> {t("Created at: {{time}}", { time: formattedDate(page.createdAt), })}
); } function ConnectionWarning() { const { t } = useTranslation(); const yjsConnectionStatus = useAtomValue(yjsConnectionStatusAtom); const [showWarning, setShowWarning] = useState(false); const timeoutRef = useRef | null>(null); useEffect(() => { const isDisconnected = ["disconnected", "connecting"].includes( yjsConnectionStatus, ); if (isDisconnected) { if (!timeoutRef.current) { timeoutRef.current = setTimeout(() => setShowWarning(true), 5000); } } else { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } setShowWarning(false); } }, [yjsConnectionStatus]); // Cleanup only on unmount useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); if (!showWarning) return null; return ( ); }