// @flow import { observer } from "mobx-react"; import { EditIcon, PinIcon, StarredIcon, UnstarredIcon, DuplicateIcon, ArchiveIcon, TrashIcon, MoveIcon, HistoryIcon, UnpublishIcon, ShapesIcon, PrintIcon, ImportIcon, NewDocumentIcon, DownloadIcon, BuildingBlocksIcon, RestoreIcon, CrossIcon, } from "outline-icons"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; import { useMenuState, MenuButton } from "reakit/Menu"; import { VisuallyHidden } from "reakit/VisuallyHidden"; import styled from "styled-components"; import Document from "models/Document"; import DocumentDelete from "scenes/DocumentDelete"; import DocumentMove from "scenes/DocumentMove"; import DocumentPermanentDelete from "scenes/DocumentPermanentDelete"; import DocumentTemplatize from "scenes/DocumentTemplatize"; import CollectionIcon from "components/CollectionIcon"; import ContextMenu from "components/ContextMenu"; import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton"; import Template from "components/ContextMenu/Template"; import Flex from "components/Flex"; import Modal from "components/Modal"; import useStores from "hooks/useStores"; import useToasts from "hooks/useToasts"; import getDataTransferFiles from "utils/getDataTransferFiles"; import { documentHistoryUrl, documentUrl, editDocumentUrl, newDocumentUrl, } from "utils/routeHelpers"; type Props = {| document: Document, className: string, isRevision?: boolean, showPrint?: boolean, modal?: boolean, showToggleEmbeds?: boolean, showPin?: boolean, label?: (any) => React.Node, onOpen?: () => void, onClose?: () => void, |}; function DocumentMenu({ document, isRevision, className, modal = true, showToggleEmbeds, showPrint, showPin, label, onOpen, onClose, }: Props) { const { policies, collections, documents } = useStores(); const { showToast } = useToasts(); const menu = useMenuState({ modal, unstable_preventOverflow: true, unstable_fixed: true, unstable_flip: true, }); const history = useHistory(); const { t } = useTranslation(); const [renderModals, setRenderModals] = React.useState(false); const [showDeleteModal, setShowDeleteModal] = React.useState(false); const [ showPermanentDeleteModal, setShowPermanentDeleteModal, ] = React.useState(false); const [showMoveModal, setShowMoveModal] = React.useState(false); const [showTemplateModal, setShowTemplateModal] = React.useState(false); const file = React.useRef<?HTMLInputElement>(); const handleOpen = React.useCallback(() => { setRenderModals(true); if (onOpen) { onOpen(); } }, [onOpen]); const handleDuplicate = React.useCallback( async (ev: SyntheticEvent<>) => { const duped = await document.duplicate(); // when duplicating, go straight to the duplicated document content history.push(duped.url); showToast(t("Document duplicated"), { type: "success" }); }, [t, history, showToast, document] ); const handleArchive = React.useCallback( async (ev: SyntheticEvent<>) => { await document.archive(); showToast(t("Document archived"), { type: "success" }); }, [showToast, t, document] ); const handleRestore = React.useCallback( async (ev: SyntheticEvent<>, options?: { collectionId: string }) => { await document.restore(options); showToast(t("Document restored"), { type: "success" }); }, [showToast, t, document] ); const handleUnpublish = React.useCallback( async (ev: SyntheticEvent<>) => { await document.unpublish(); showToast(t("Document unpublished"), { type: "success" }); }, [showToast, t, document] ); const handlePrint = React.useCallback( (ev: SyntheticEvent<>) => { menu.hide(); window.print(); }, [menu] ); const handleStar = React.useCallback( (ev: SyntheticEvent<>) => { ev.preventDefault(); ev.stopPropagation(); document.star(); }, [document] ); const handleUnstar = React.useCallback( (ev: SyntheticEvent<>) => { ev.preventDefault(); ev.stopPropagation(); document.unstar(); }, [document] ); const collection = collections.get(document.collectionId); const can = policies.abilities(document.id); const canViewHistory = can.read && !can.restore; const restoreItems = React.useMemo( () => [ ...collections.orderedData.reduce((filtered, collection) => { const can = policies.abilities(collection.id); if (can.update) { filtered.push({ onClick: (ev) => handleRestore(ev, { collectionId: collection.id }), title: ( <Flex align="center"> <CollectionIcon collection={collection} /> <CollectionName>{collection.name}</CollectionName> </Flex> ), }); } return filtered; }, []), ], [collections.orderedData, handleRestore, policies] ); const stopPropagation = React.useCallback((ev: SyntheticEvent<>) => { ev.stopPropagation(); }, []); const handleImportDocument = React.useCallback( (ev: SyntheticEvent<>) => { ev.preventDefault(); ev.stopPropagation(); // simulate a click on the file upload input element if (file.current) { file.current.click(); } }, [file] ); const handleFilePicked = React.useCallback( async (ev: SyntheticEvent<>) => { const files = getDataTransferFiles(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) { showToast(err.message, { type: "error", }); throw err; } }, [history, showToast, collection, documents, document.id] ); return ( <> <VisuallyHidden> <input type="file" ref={file} onChange={handleFilePicked} onClick={stopPropagation} accept={documents.importFileTypes.join(", ")} tabIndex="-1" /> </VisuallyHidden> {label ? ( <MenuButton {...menu}>{label}</MenuButton> ) : ( <OverflowMenuButton className={className} aria-label={t("Show menu")} {...menu} /> )} <ContextMenu {...menu} aria-label={t("Document options")} onOpen={handleOpen} onClose={onClose} > <Template {...menu} items={[ { title: t("Restore"), visible: (!!collection && can.restore) || can.unarchive, onClick: handleRestore, icon: <RestoreIcon />, }, { title: t("Restore"), visible: !collection && !!can.restore && restoreItems.length !== 0, style: { left: -170, position: "relative", top: -40, }, icon: <RestoreIcon />, hover: true, items: [ { type: "heading", title: t("Choose a collection"), }, ...restoreItems, ], }, { title: t("Unpin"), onClick: document.unpin, visible: !!(showPin && document.pinned && can.unpin), icon: <PinIcon />, }, { title: t("Pin to collection"), onClick: document.pin, visible: !!(showPin && !document.pinned && can.pin), icon: <PinIcon />, }, { title: t("Unstar"), onClick: handleUnstar, visible: document.isStarred && !!can.unstar, icon: <UnstarredIcon />, }, { title: t("Star"), onClick: handleStar, visible: !document.isStarred && !!can.star, icon: <StarredIcon />, }, { type: "separator", }, { title: t("Edit"), to: editDocumentUrl(document), visible: !!can.update, icon: <EditIcon />, }, { title: t("New nested document"), to: newDocumentUrl(document.collectionId, { parentDocumentId: document.id, }), visible: !!can.createChildDocument, icon: <NewDocumentIcon />, }, { title: t("Import document"), visible: can.createChildDocument, onClick: handleImportDocument, icon: <ImportIcon />, }, { title: `${t("Create template")}…`, onClick: () => setShowTemplateModal(true), visible: !!can.update && !document.isTemplate, icon: <ShapesIcon />, }, { title: t("Duplicate"), onClick: handleDuplicate, visible: !!can.update, icon: <DuplicateIcon />, }, { title: t("Unpublish"), onClick: handleUnpublish, visible: !!can.unpublish, icon: <UnpublishIcon />, }, { title: t("Archive"), onClick: handleArchive, visible: !!can.archive, icon: <ArchiveIcon />, }, { title: `${t("Delete")}…`, onClick: () => setShowDeleteModal(true), visible: !!can.delete, icon: <TrashIcon />, }, { title: `${t("Permanently delete")}…`, onClick: () => setShowPermanentDeleteModal(true), visible: can.permanentDelete, icon: <CrossIcon />, }, { title: `${t("Move")}…`, onClick: () => setShowMoveModal(true), visible: !!can.move, icon: <MoveIcon />, }, { title: t("Enable embeds"), onClick: document.enableEmbeds, visible: !!showToggleEmbeds && document.embedsDisabled, icon: <BuildingBlocksIcon />, }, { title: t("Disable embeds"), onClick: document.disableEmbeds, visible: !!showToggleEmbeds && !document.embedsDisabled, icon: <BuildingBlocksIcon />, }, { type: "separator", }, { title: t("History"), to: isRevision ? documentUrl(document) : documentHistoryUrl(document), visible: canViewHistory, icon: <HistoryIcon />, }, { title: t("Download"), onClick: document.download, visible: !!can.download, icon: <DownloadIcon />, }, { title: t("Print"), onClick: handlePrint, visible: !!showPrint, icon: <PrintIcon />, }, ]} /> </ContextMenu> {renderModals && ( <> {can.move && ( <Modal title={t("Move {{ documentName }}", { documentName: document.noun, })} onRequestClose={() => setShowMoveModal(false)} isOpen={showMoveModal} > <DocumentMove document={document} onRequestClose={() => setShowMoveModal(false)} /> </Modal> )} {can.delete && ( <Modal title={t("Delete {{ documentName }}", { documentName: document.noun, })} onRequestClose={() => setShowDeleteModal(false)} isOpen={showDeleteModal} > <DocumentDelete document={document} onSubmit={() => setShowDeleteModal(false)} /> </Modal> )} {can.permanentDelete && ( <Modal title={t("Permanently delete {{ documentName }}", { documentName: document.noun, })} onRequestClose={() => setShowPermanentDeleteModal(false)} isOpen={showPermanentDeleteModal} > <DocumentPermanentDelete document={document} onSubmit={() => setShowPermanentDeleteModal(false)} /> </Modal> )} {can.update && ( <Modal title={t("Create template")} onRequestClose={() => setShowTemplateModal(false)} isOpen={showTemplateModal} > <DocumentTemplatize document={document} onSubmit={() => setShowTemplateModal(false)} /> </Modal> )} </> )} </> ); } const CollectionName = styled.div` overflow: hidden; white-space: nowrap; text-overflow: ellipsis; `; export default observer(DocumentMenu);