diff --git a/app/actions/definitions/documents.js b/app/actions/definitions/documents.js index 15a8b54d..90b43d3c 100644 --- a/app/actions/definitions/documents.js +++ b/app/actions/definitions/documents.js @@ -1,11 +1,18 @@ // @flow +import invariant from "invariant"; import { + DownloadIcon, + DuplicateIcon, StarredIcon, + PrintIcon, + UnstarredIcon, DocumentIcon, NewDocumentIcon, + ShapesIcon, ImportIcon, } from "outline-icons"; import * as React from "react"; +import DocumentTemplatize from "scenes/DocumentTemplatize"; import { createAction } from "actions"; import { DocumentSection } from "actions/sections"; import getDataTransferFiles from "utils/getDataTransferFiles"; @@ -50,15 +57,113 @@ export const createDocument = createAction({ activeCollectionId && history.push(newDocumentPath(activeCollectionId)), }); +export const starDocument = createAction({ + name: ({ t }) => t("Star"), + section: DocumentSection, + icon: , + keywords: "favorite bookmark", + visible: ({ activeDocumentId, stores }) => { + if (!activeDocumentId) return false; + const document = stores.documents.get(activeDocumentId); + + return ( + !document?.isStarred && stores.policies.abilities(activeDocumentId).star + ); + }, + perform: ({ activeDocumentId, stores }) => { + if (!activeDocumentId) return false; + + const document = stores.documents.get(activeDocumentId); + document?.star(); + }, +}); + +export const unstarDocument = createAction({ + name: ({ t }) => t("Unstar"), + section: DocumentSection, + icon: , + keywords: "unfavorite unbookmark", + visible: ({ activeDocumentId, stores }) => { + if (!activeDocumentId) return false; + const document = stores.documents.get(activeDocumentId); + + return ( + !!document?.isStarred && + stores.policies.abilities(activeDocumentId).unstar + ); + }, + perform: ({ activeDocumentId, stores }) => { + if (!activeDocumentId) return false; + + const document = stores.documents.get(activeDocumentId); + document?.unstar(); + }, +}); + +export const downloadDocument = createAction({ + name: ({ t, isContextMenu }) => + isContextMenu ? t("Download") : t("Download document"), + section: DocumentSection, + icon: , + keywords: "export", + visible: ({ activeDocumentId, stores }) => + !!activeDocumentId && stores.policies.abilities(activeDocumentId).download, + perform: ({ activeDocumentId, stores }) => { + if (!activeDocumentId) return false; + + const document = stores.documents.get(activeDocumentId); + document?.download(); + }, +}); + +export const duplicateDocument = createAction({ + name: ({ t, isContextMenu }) => + isContextMenu ? t("Duplicate") : t("Duplicate document"), + section: DocumentSection, + icon: , + keywords: "copy", + visible: ({ activeDocumentId, stores }) => + !!activeDocumentId && stores.policies.abilities(activeDocumentId).update, + perform: async ({ activeDocumentId, t, stores }) => { + if (!activeDocumentId) return false; + + const document = stores.documents.get(activeDocumentId); + invariant(document, "Document must exist"); + + const duped = await document.duplicate(); + + // when duplicating, go straight to the duplicated document content + history.push(duped.url); + stores.toasts.showToast(t("Document duplicated"), { type: "success" }); + }, +}); + +export const printDocument = createAction({ + name: ({ t, isContextMenu }) => + isContextMenu ? t("Print") : t("Print document"), + section: DocumentSection, + icon: , + visible: ({ activeDocumentId }) => !!activeDocumentId, + perform: async () => { + window.print(); + }, +}); + export const importDocument = createAction({ - name: ({ t }) => t("Import document"), + name: ({ t, activeDocumentId }) => t("Import document"), section: DocumentSection, icon: , keywords: "upload", - visible: ({ activeCollectionId, stores }) => - !!activeCollectionId && - stores.policies.abilities(activeCollectionId).update, - perform: ({ activeCollectionId, stores }) => { + visible: ({ activeCollectionId, activeDocumentId, stores }) => { + if (activeDocumentId) { + return !!stores.policies.abilities(activeDocumentId).createChildDocument; + } + if (activeCollectionId) { + return !!stores.policies.abilities(activeCollectionId).update; + } + return false; + }, + perform: ({ activeCollectionId, activeDocumentId, stores }) => { const { documents, toasts } = stores; const input = document.createElement("input"); @@ -71,7 +176,7 @@ export const importDocument = createAction({ const file = files[0]; const document = await documents.import( file, - null, + activeDocumentId, activeCollectionId, { publish: true, @@ -90,8 +195,47 @@ export const importDocument = createAction({ }, }); +export const createTemplate = createAction({ + name: ({ t }) => t("Templatize"), + section: DocumentSection, + icon: , + keywords: "new create template", + visible: ({ activeCollectionId, activeDocumentId, stores }) => { + if (!activeDocumentId) return false; + + const document = stores.documents.get(activeDocumentId); + invariant(document, "Document must exist"); + + return ( + !!activeCollectionId && + stores.policies.abilities(activeCollectionId).update && + !document.isTemplate + ); + }, + perform: ({ activeDocumentId, stores, t, event }) => { + event?.preventDefault(); + event?.stopPropagation(); + + stores.dialogs.openModal({ + title: t("Create template"), + content: ( + + ), + }); + }, +}); + export const rootDocumentActions = [ openDocument, createDocument, + createTemplate, importDocument, + downloadDocument, + starDocument, + unstarDocument, + duplicateDocument, + printDocument, ]; diff --git a/app/components/CommandBar.js b/app/components/CommandBar.js index 9ca483c3..2f6a8985 100644 --- a/app/components/CommandBar.js +++ b/app/components/CommandBar.js @@ -88,6 +88,10 @@ const Animator = styled(KBarAnimator)` ${breakpoint("desktopLarge")` max-width: 740px; `}; + + @media print { + display: none; + } `; export default observer(CommandBar); diff --git a/app/hooks/useCommandBarActions.js b/app/hooks/useCommandBarActions.js index 2a76c872..a14f896b 100644 --- a/app/hooks/useCommandBarActions.js +++ b/app/hooks/useCommandBarActions.js @@ -26,5 +26,8 @@ export default function useCommandBarActions(actions: Action[]) { actions.map((action) => actionToKBar(action, context)) ); - useRegisterActions(registerable, [registerable.length, location.pathname]); + useRegisterActions(registerable, [ + ...registerable.map((r) => r.id), + location.pathname, + ]); } diff --git a/app/menus/DocumentMenu.js b/app/menus/DocumentMenu.js index e8aa4310..a0a8686d 100644 --- a/app/menus/DocumentMenu.js +++ b/app/menus/DocumentMenu.js @@ -479,7 +479,7 @@ function DocumentMenu({ isOpen={showTemplateModal} > setShowTemplateModal(false)} /> diff --git a/app/scenes/DocumentTemplatize.js b/app/scenes/DocumentTemplatize.js index b4cec0d7..6314a3af 100644 --- a/app/scenes/DocumentTemplatize.js +++ b/app/scenes/DocumentTemplatize.js @@ -1,26 +1,30 @@ // @flow +import invariant from "invariant"; import { observer } from "mobx-react"; import * as React from "react"; import { useState } from "react"; import { useTranslation, Trans } from "react-i18next"; import { useHistory } from "react-router-dom"; -import Document from "models/Document"; import Button from "components/Button"; import Flex from "components/Flex"; import HelpText from "components/HelpText"; +import useStores from "hooks/useStores"; import useToasts from "hooks/useToasts"; import { documentUrl } from "utils/routeHelpers"; type Props = { - document: Document, + documentId: string, onSubmit: () => void, }; -function DocumentTemplatize({ document, onSubmit }: Props) { +function DocumentTemplatize({ documentId, onSubmit }: Props) { const [isSaving, setIsSaving] = useState(); const history = useHistory(); const { showToast } = useToasts(); const { t } = useTranslation(); + const { documents } = useStores(); + const document = documents.get(documentId); + invariant(document, "Document must exist"); const handleSubmit = React.useCallback( async (ev: SyntheticEvent<>) => {