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<>) => {