import { observer } from "mobx-react"; import { EditIcon, InputIcon, RestoreIcon, SearchIcon } from "outline-icons"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; import { useMenuState, MenuButton, MenuButtonHTMLProps } from "reakit/Menu"; import { VisuallyHidden } from "reakit/VisuallyHidden"; import { toast } from "sonner"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; import { s } from "@shared/styles"; import { UserPreference } from "@shared/types"; import { getEventFiles } from "@shared/utils/files"; import Document from "~/models/Document"; import ContextMenu from "~/components/ContextMenu"; import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton"; import Separator from "~/components/ContextMenu/Separator"; import Template from "~/components/ContextMenu/Template"; import CollectionIcon from "~/components/Icons/CollectionIcon"; import Switch from "~/components/Switch"; import { actionToMenuItem } from "~/actions"; import { pinDocument, createTemplate, subscribeDocument, unsubscribeDocument, moveDocument, deleteDocument, permanentlyDeleteDocument, downloadDocument, importDocument, starDocument, unstarDocument, duplicateDocument, archiveDocument, openDocumentHistory, openDocumentInsights, publishDocument, unpublishDocument, printDocument, openDocumentComments, createDocumentFromTemplate, createNestedDocument, shareDocument, copyDocument, searchInDocument, } from "~/actions/definitions/documents"; import useActionContext from "~/hooks/useActionContext"; import useCurrentUser from "~/hooks/useCurrentUser"; import useMobile from "~/hooks/useMobile"; import usePolicy from "~/hooks/usePolicy"; import useRequest from "~/hooks/useRequest"; import useStores from "~/hooks/useStores"; import { MenuItem } from "~/types"; import { documentEditPath } from "~/utils/routeHelpers"; type Props = { document: Document; className?: string; isRevision?: boolean; /** Pass true if the document is currently being displayed */ showDisplayOptions?: boolean; modal?: boolean; visible?: boolean; showToggleEmbeds?: boolean; showPin?: boolean; label?: (props: MenuButtonHTMLProps) => React.ReactNode; onFindAndReplace?: () => void; onRename?: () => void; onOpen?: () => void; onClose?: () => void; }; function DocumentMenu({ document, className, modal = true, visible, showToggleEmbeds, showDisplayOptions, label, onFindAndReplace, onRename, onOpen, onClose, }: Props) { const user = useCurrentUser(); const { policies, collections, documents, subscriptions } = useStores(); const menu = useMenuState({ modal, unstable_preventOverflow: true, unstable_fixed: true, unstable_flip: true, }); const history = useHistory(); const context = useActionContext({ isContextMenu: true, activeDocumentId: document.id, activeCollectionId: document.collectionId ?? undefined, }); const { t } = useTranslation(); const isMobile = useMobile(); const file = React.useRef(null); const { data, loading, request } = useRequest(() => subscriptions.fetchPage({ documentId: document.id, event: "documents.update", }) ); React.useEffect(() => { if (visible !== undefined && menu.visible !== visible) { menu.setVisible(visible); } }, [visible]); const handleOpen = React.useCallback(async () => { if (!data && !loading) { await request(); } if (onOpen) { onOpen(); } }, [data, loading, onOpen, request]); const handleRestore = React.useCallback( async ( ev: React.SyntheticEvent, options?: { collectionId: string; } ) => { await document.restore(options); toast.success(t("Document restored")); }, [t, document] ); const collection = document.collectionId ? collections.get(document.collectionId) : undefined; const can = usePolicy(document); const restoreItems = React.useMemo( () => [ ...collections.orderedData.reduce((filtered, collection) => { const can = policies.abilities(collection.id); if (can.createDocument) { filtered.push({ type: "button", onClick: (ev) => handleRestore(ev, { collectionId: collection.id, }), icon: , title: collection.name, }); } return filtered; }, []), ], [collections.orderedData, handleRestore, policies] ); const stopPropagation = React.useCallback((ev: React.SyntheticEvent) => { ev.stopPropagation(); }, []); const handleFilePicked = React.useCallback( async (ev: React.ChangeEvent) => { const files = getEventFiles(ev); // Because this is the onChange handler it's possible for the change to be // from previously selecting a file to not selecting a file – aka empty if (!files.length) { return; } if (!collection) { return; } try { const file = files[0]; const importedDocument = await documents.import( file, document.id, collection.id, { publish: true, } ); history.push(importedDocument.url); } catch (err) { toast.error(err.message); throw err; } }, [history, collection, documents, document.id] ); return ( <> {label ? ( {label} ) : ( )}