diff --git a/app/actions/definitions/collections.tsx b/app/actions/definitions/collections.tsx
index 1bee6bb9f5..6107f73440 100644
--- a/app/actions/definitions/collections.tsx
+++ b/app/actions/definitions/collections.tsx
@@ -29,8 +29,8 @@ import DynamicCollectionIcon from "~/components/Icons/CollectionIcon";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import {
createAction,
- createActionWithChildren,
createInternalLinkAction,
+ createActionWithChildren,
} from "~/actions";
import { ActiveCollectionSection, CollectionSection } from "~/actions/sections";
import { setPersistedState } from "~/hooks/usePersistedState";
@@ -530,15 +530,9 @@ export const createTemplate = createInternalLinkAction({
getActivePolicies(Collection).some(
(policy) => policy.abilities.createDocument
),
- to: ({ getActiveModel, sidebarContext }) => {
+ to: ({ getActiveModel }) => {
const collection = getActiveModel(Collection);
- const [pathname, search] = newTemplatePath(collection?.id).split("?");
-
- return {
- pathname,
- search,
- state: { sidebarContext },
- };
+ return newTemplatePath(collection?.id);
},
});
diff --git a/app/actions/definitions/documents.tsx b/app/actions/definitions/documents.tsx
index 8103100fe9..d5c81327d3 100644
--- a/app/actions/definitions/documents.tsx
+++ b/app/actions/definitions/documents.tsx
@@ -42,12 +42,11 @@ import { Week } from "@shared/utils/time";
import type UserMembership from "~/models/UserMembership";
import { client } from "~/utils/ApiClient";
import DocumentDelete from "~/scenes/DocumentDelete";
-import DocumentMove from "~/scenes/DocumentMove";
import DocumentPermanentDelete from "~/scenes/DocumentPermanentDelete";
import DocumentPublish from "~/scenes/DocumentPublish";
import DeleteDocumentsInTrash from "~/scenes/Trash/components/DeleteDocumentsInTrash";
import ConfirmationDialog from "~/components/ConfirmationDialog";
-import DocumentCopy from "~/components/DocumentCopy";
+import DocumentCopy from "~/components/DocumentExplorer/DocumentCopy";
import { DocumentDownload } from "~/components/DocumentDownload";
import MarkdownIcon from "~/components/Icons/MarkdownIcon";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
@@ -87,6 +86,7 @@ import type {
} from "~/types";
import lazyWithRetry from "~/utils/lazyWithRetry";
import env from "~/env";
+import DocumentMove from "~/components/DocumentExplorer/DocumentMove";
const Insights = lazyWithRetry(
() => import("~/scenes/Document/components/Insights")
@@ -138,18 +138,13 @@ export const editDocument = createInternalLinkAction({
keywords: "edit",
icon: ,
visible: ({ activeDocumentId, stores }) => {
- const { auth, documents, policies } = stores;
+ const { auth, policies } = stores;
- const document = activeDocumentId
- ? documents.get(activeDocumentId)
- : undefined;
const can = activeDocumentId
? policies.abilities(activeDocumentId)
: undefined;
- return (
- !!can?.update && !!auth.user?.separateEditMode && !document?.template
- );
+ return !!can?.update && !!auth.user?.separateEditMode;
},
to: ({ activeDocumentId, stores }) => {
const document = activeDocumentId
@@ -222,12 +217,7 @@ export const createDocumentFromTemplate = createInternalLinkAction({
? stores.documents.get(activeDocumentId)
: undefined;
- if (
- !currentTeamId ||
- !document?.isTemplate ||
- !!document?.isDraft ||
- !!document?.isDeleted
- ) {
+ if (!currentTeamId || !!document?.isDraft || !!document?.isDeleted) {
return false;
}
@@ -468,7 +458,7 @@ export const publishDocument = createAction({
return;
}
- if (document?.collectionId || document?.template) {
+ if (document?.collectionId) {
await document.save(undefined, {
publish: true,
});
@@ -1055,7 +1045,7 @@ export const createTemplateFromDocument = createAction({
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
- if (document?.isTemplate || !document?.isActive) {
+ if (!document?.isActive) {
return false;
}
return !!(
@@ -1107,46 +1097,8 @@ export const searchDocumentsForQuery = (query: string) =>
visible: ({ location }) => location.pathname !== searchPath(),
});
-export const moveTemplateToWorkspace = createAction({
- name: ({ t }) => t("Move to workspace"),
- analyticsName: "Move template to workspace",
- section: DocumentSection,
- icon: ,
- iconInContextMenu: false,
- visible: ({ activeDocumentId, stores }) => {
- if (!activeDocumentId) {
- return false;
- }
- const document = stores.documents.get(activeDocumentId);
- if (!document || !document.template || document.isWorkspaceTemplate) {
- return false;
- }
- return !!stores.policies.abilities(activeDocumentId).move;
- },
- perform: async ({ activeDocumentId, stores }) => {
- if (activeDocumentId) {
- const document = stores.documents.get(activeDocumentId);
- if (!document) {
- return;
- }
-
- await document.move({
- collectionId: null,
- });
- }
- },
-});
-
export const moveDocumentToCollection = createAction({
- name: ({ activeDocumentId, stores, t }) => {
- if (!activeDocumentId) {
- return t("Move");
- }
- const document = stores.documents.get(activeDocumentId);
- return document?.template && document?.collectionId
- ? t("Move to collection")
- : t("Move");
- },
+ name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: ActiveDocumentSection,
icon: ,
@@ -1184,8 +1136,7 @@ export const moveDocument = createAction({
return false;
}
const document = stores.documents.get(activeDocumentId);
- // Don't show the button if this is a non-workspace template.
- if (!document || (document.template && !document.isWorkspaceTemplate)) {
+ if (!document) {
return false;
}
return !!stores.policies.abilities(activeDocumentId).move;
@@ -1193,25 +1144,6 @@ export const moveDocument = createAction({
perform: moveDocumentToCollection.perform,
});
-export const moveTemplate = createActionWithChildren({
- name: ({ t }) => t("Move"),
- analyticsName: "Move document",
- section: ActiveDocumentSection,
- icon: ,
- visible: ({ activeDocumentId, stores }) => {
- if (!activeDocumentId) {
- return false;
- }
- const document = stores.documents.get(activeDocumentId);
- // Don't show the menu if this is not a template (or) a workspace template.
- if (!document || !document.template || document.isWorkspaceTemplate) {
- return false;
- }
- return !!stores.policies.abilities(activeDocumentId).move;
- },
- children: [moveTemplateToWorkspace, moveDocumentToCollection],
-});
-
export const archiveDocument = createAction({
name: ({ t }) => `${t("Archive")}…`,
analyticsName: "Archive document",
@@ -1270,10 +1202,7 @@ export const restoreDocument = createAction({
: undefined;
const can = stores.policies.abilities(document.id);
- return (
- !!(document.isWorkspaceTemplate || collection?.isActive) &&
- !!(can.restore || can.unarchive)
- );
+ return !!collection?.isActive && !!(can.restore || can.unarchive);
},
perform: async ({ t, stores, activeDocumentId }) => {
const document = activeDocumentId
@@ -1310,10 +1239,7 @@ export const restoreDocumentToCollection = createActionWithChildren({
? stores.collections.get(document.collectionId)
: undefined;
- return (
- !(document.isWorkspaceTemplate || collection?.isActive) &&
- !!(can.restore || can.unarchive)
- );
+ return !collection?.isActive && !!(can.restore || can.unarchive);
},
children: ({ t, activeDocumentId, stores }) => {
const { collections, documents, policies } = stores;
@@ -1498,12 +1424,7 @@ export const openDocumentInsights = createAction({
? stores.documents.get(activeDocumentId)
: undefined;
- return (
- !!activeDocumentId &&
- can.listViews &&
- !document?.isTemplate &&
- !document?.isDeleted
- );
+ return !!activeDocumentId && can.listViews && !document?.isDeleted;
},
perform: ({ activeDocumentId, stores, t }) => {
const document = activeDocumentId
@@ -1604,7 +1525,6 @@ export const rootDocumentActions = [
searchInDocument,
duplicateDocument,
leaveDocument,
- moveTemplateToWorkspace,
moveDocumentToCollection,
openRandomDocument,
permanentlyDeleteDocument,
diff --git a/app/actions/definitions/templates.tsx b/app/actions/definitions/templates.tsx
new file mode 100644
index 0000000000..47eeabd993
--- /dev/null
+++ b/app/actions/definitions/templates.tsx
@@ -0,0 +1,224 @@
+import copy from "copy-to-clipboard";
+import {
+ CaseSensitiveIcon,
+ CollectionIcon,
+ CopyIcon,
+ MoveIcon,
+ NewDocumentIcon,
+ PlusIcon,
+ PrintIcon,
+ TrashIcon,
+} from "outline-icons";
+import { Trans } from "react-i18next";
+import { toast } from "sonner";
+import ConfirmationDialog from "~/components/ConfirmationDialog";
+import TemplateMove from "~/components/DocumentExplorer/TemplateMove";
+import {
+ createAction,
+ createActionWithChildren,
+ createInternalLinkAction,
+} from "~/actions";
+import { newDocumentPath, newTemplatePath, urlify } from "~/utils/routeHelpers";
+import { ActiveTemplateSection, TemplateSection } from "../sections";
+import Template from "~/models/Template";
+import { AvatarSize } from "~/components/Avatar";
+import TeamLogo from "~/components/TeamLogo";
+
+export const createTemplate = createInternalLinkAction({
+ name: ({ t }) => t("New template"),
+ analyticsName: "New template",
+ section: TemplateSection,
+ icon: ,
+ keywords: "new create template",
+ visible: ({ currentTeamId, stores }) =>
+ !!stores.policies.abilities(currentTeamId!).createTemplate,
+ to: newTemplatePath(),
+});
+
+export const deleteTemplate = createAction({
+ name: ({ t }) => `${t("Delete")}…`,
+ analyticsName: "Delete template",
+ section: ActiveTemplateSection,
+ icon: ,
+ dangerous: true,
+ visible: ({ getActivePolicies }) =>
+ getActivePolicies(Template).some((policy) => policy.abilities.delete),
+ perform: ({ getActiveModel, stores, t }) => {
+ const template = getActiveModel(Template);
+ if (!template) {
+ return;
+ }
+
+ stores.dialogs.openModal({
+ title: t("Delete {{ documentName }}", {
+ documentName: t("template"),
+ }),
+ content: (
+ {
+ await template.delete();
+ toast.success(t("Template deleted"));
+ }}
+ savingText={`${t("Deleting")}…`}
+ danger
+ >
+ ,
+ }}
+ />
+
+ ),
+ });
+ },
+});
+
+export const moveTemplateToWorkspace = createAction({
+ name: ({ t }) => t("Move to workspace"),
+ analyticsName: "Move template to workspace",
+ section: ActiveTemplateSection,
+ icon: ({ stores }) => {
+ const { team } = stores.auth;
+ return ;
+ },
+ visible: ({ getActiveModel }) => {
+ const template = getActiveModel(Template);
+ return !!template?.collectionId;
+ },
+ perform: async ({ getActiveModel, stores, t }) => {
+ const template = getActiveModel(Template);
+ if (!template) {
+ return;
+ }
+
+ try {
+ await template.save({ collectionId: null });
+ toast.success(t("Template moved"));
+ stores.dialogs.closeAllModals();
+ } catch (_err) {
+ toast.error(t("Couldn't move the template, try again?"));
+ }
+ },
+});
+
+export const moveTemplateToCollection = createAction({
+ name: ({ t }) => t("Move to collection"),
+ analyticsName: "Move template to collection",
+ section: ActiveTemplateSection,
+ icon: ,
+ perform: ({ getActiveModel, stores, t }) => {
+ const template = getActiveModel(Template);
+ if (!template) {
+ return;
+ }
+
+ stores.dialogs.openModal({
+ title: t("Move template"),
+ content: ,
+ });
+ },
+});
+
+export const moveTemplate = createActionWithChildren({
+ name: ({ t }) => t("Move"),
+ analyticsName: "Move template",
+ section: ActiveTemplateSection,
+ icon: ,
+ visible: ({ getActivePolicies }) =>
+ getActivePolicies(Template).some((policy) => policy.abilities.move),
+ children: [moveTemplateToWorkspace, moveTemplateToCollection],
+});
+
+export const createDocumentFromTemplate = createInternalLinkAction({
+ name: ({ t }) => t("New document"),
+ analyticsName: "New document from template",
+ section: ActiveTemplateSection,
+ icon: ,
+ keywords: "create",
+ visible: ({ currentTeamId, getActiveModel, stores }) => {
+ const template = getActiveModel(Template);
+ if (!template || !currentTeamId) {
+ return false;
+ }
+
+ if (template.collectionId) {
+ return !!stores.policies.abilities(template.collectionId).createDocument;
+ }
+ return !!stores.policies.abilities(currentTeamId).createDocument;
+ },
+ to: ({ getActiveModel, activeCollectionId, sidebarContext }) => {
+ const template = getActiveModel(Template);
+ if (!template) {
+ return "";
+ }
+ const collectionId = template?.collectionId ?? activeCollectionId;
+
+ const [pathname, search] = newDocumentPath(collectionId, {
+ templateId: template.id,
+ }).split("?");
+
+ return {
+ pathname,
+ search,
+ state: { sidebarContext },
+ };
+ },
+});
+
+export const copyTemplateLink = createAction({
+ name: ({ t }) => t("Copy link"),
+ analyticsName: "Copy template link",
+ section: ActiveTemplateSection,
+ icon: ,
+ iconInContextMenu: false,
+ perform: ({ getActiveModel, t }) => {
+ const template = getActiveModel(Template);
+ if (template) {
+ copy(urlify(template.path));
+ toast.success(t("Link copied to clipboard"));
+ }
+ },
+});
+
+export const copyTemplateAsPlainText = createAction({
+ name: ({ t }) => t("Copy as text"),
+ analyticsName: "Copy template as text",
+ section: ActiveTemplateSection,
+ icon: ,
+ iconInContextMenu: false,
+ perform: async ({ getActiveModel, t }) => {
+ const template = getActiveModel(Template);
+ if (template) {
+ const { ProsemirrorHelper } =
+ await import("~/models/helpers/ProsemirrorHelper");
+ copy(ProsemirrorHelper.toPlainText(template));
+ toast.success(t("Text copied to clipboard"));
+ }
+ },
+});
+
+export const copyTemplate = createActionWithChildren({
+ name: ({ t }) => t("Copy"),
+ analyticsName: "Copy template",
+ section: ActiveTemplateSection,
+ icon: ,
+ keywords: "clipboard",
+ children: [copyTemplateLink, copyTemplateAsPlainText],
+});
+
+export const printTemplate = createAction({
+ name: ({ t, isMenu }) => (isMenu ? t("Print") : t("Print template")),
+ analyticsName: "Print template",
+ section: ActiveTemplateSection,
+ icon: ,
+ visible: ({ getActiveModel }) => !!getActiveModel(Template) && !!window.print,
+ perform: () => {
+ queueMicrotask(window.print);
+ },
+});
+
+export const rootTemplateActions = [moveTemplate, createDocumentFromTemplate];
diff --git a/app/actions/sections.ts b/app/actions/sections.ts
index df2ddd9139..ef55a448c7 100644
--- a/app/actions/sections.ts
+++ b/app/actions/sections.ts
@@ -24,6 +24,15 @@ export const ActiveDocumentSection = ({ t, stores }: ActionContext) => {
ActiveDocumentSection.priority = 0.9;
+export const TemplateSection = ({ t }: ActionContext) => t("Template");
+
+export const ActiveTemplateSection = ({ t, stores }: ActionContext) => {
+ const activeTemplate = stores.templates.active;
+ return `${t("Template")} · ${activeTemplate?.titleWithDefault}`;
+};
+
+ActiveTemplateSection.priority = 0.9;
+
export const RecentSection = ({ t }: ActionContext) => t("Recently viewed");
RecentSection.priority = 1;
diff --git a/app/components/CommandBar/useTemplatesAction.tsx b/app/components/CommandBar/useTemplatesAction.tsx
index faed008fea..0d82c7a6c7 100644
--- a/app/components/CommandBar/useTemplatesAction.tsx
+++ b/app/components/CommandBar/useTemplatesAction.tsx
@@ -11,15 +11,15 @@ import useStores from "~/hooks/useStores";
import { newDocumentPath } from "~/utils/routeHelpers";
const useTemplatesAction = () => {
- const { documents } = useStores();
+ const { templates } = useStores();
useEffect(() => {
- void documents.fetchAllTemplates();
- }, [documents]);
+ void templates.fetchAll();
+ }, [templates]);
const actions = useMemo(
() =>
- documents.templatesAlphabetical.map((template) =>
+ templates.alphabetical.map((template) =>
createInternalLinkAction({
name: template.titleWithDefault,
analyticsName: "New document",
@@ -66,7 +66,7 @@ const useTemplatesAction = () => {
},
})
),
- [documents.templatesAlphabetical]
+ [templates.alphabetical]
);
const newFromTemplate = useMemo(
diff --git a/app/components/DocumentBreadcrumb.tsx b/app/components/DocumentBreadcrumb.tsx
index df584d0868..62521b8349 100644
--- a/app/components/DocumentBreadcrumb.tsx
+++ b/app/components/DocumentBreadcrumb.tsx
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
-import { ArchiveIcon, GoToIcon, ShapesIcon, TrashIcon } from "outline-icons";
+import { ArchiveIcon, GoToIcon, TrashIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
@@ -11,7 +11,7 @@ import CollectionIcon from "~/components/Icons/CollectionIcon";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
-import { archivePath, settingsPath, trashPath } from "~/utils/routeHelpers";
+import { archivePath, trashPath } from "~/utils/routeHelpers";
import { createInternalLinkAction } from "~/actions";
import { ActiveDocumentSection } from "~/actions/sections";
@@ -67,13 +67,6 @@ function DocumentBreadcrumb(
visible: document.isArchived,
to: archivePath(),
}),
- createInternalLinkAction({
- name: t("Templates"),
- section: ActiveDocumentSection,
- icon: ,
- visible: document.template,
- to: settingsPath("templates"),
- }),
createInternalLinkAction({
name: collection?.name,
section: ActiveDocumentSection,
diff --git a/app/components/DocumentExplorer/Components.tsx b/app/components/DocumentExplorer/Components.tsx
new file mode 100644
index 0000000000..41cf330de6
--- /dev/null
+++ b/app/components/DocumentExplorer/Components.tsx
@@ -0,0 +1,17 @@
+import styled from "styled-components";
+import Flex from "../Flex";
+
+export const FlexContainer = styled(Flex)`
+ margin-left: -24px;
+ margin-right: -24px;
+ margin-bottom: -24px;
+ outline: none;
+`;
+
+export const Footer = styled(Flex)`
+ height: 64px;
+ border-top: 1px solid ${(props) => props.theme.horizontalRule};
+ padding-left: 24px;
+ padding-right: 24px;
+ flex-shrink: 0;
+`;
diff --git a/app/components/DocumentCopy.tsx b/app/components/DocumentExplorer/DocumentCopy.tsx
similarity index 69%
rename from app/components/DocumentCopy.tsx
rename to app/components/DocumentExplorer/DocumentCopy.tsx
index dec189d0b1..0210a6a078 100644
--- a/app/components/DocumentCopy.tsx
+++ b/app/components/DocumentExplorer/DocumentCopy.tsx
@@ -5,13 +5,13 @@ import { toast } from "sonner";
import styled from "styled-components";
import type { NavigationNode } from "@shared/types";
import type Document from "~/models/Document";
-import { FlexContainer, Footer, StyledText } from "~/scenes/DocumentMove";
import Button from "~/components/Button";
-import DocumentExplorer from "~/components/DocumentExplorer";
+import Switch from "~/components/Switch";
+import Text from "~/components/Text";
import useCollectionTrees from "~/hooks/useCollectionTrees";
import useStores from "~/hooks/useStores";
-import Switch from "./Switch";
-import Text from "./Text";
+import { FlexContainer, Footer } from "./Components";
+import DocumentExplorer from "./DocumentExplorer";
type Props = {
/** The original document to duplicate */
@@ -37,13 +37,8 @@ function DocumentCopy({ document, onSubmit }: Props) {
: true
);
- if (document.isTemplate) {
- return nodes
- .filter((node) => node.type === "collection")
- .map((node) => ({ ...node, children: [] }));
- }
return nodes;
- }, [policies, collectionTrees, document.isTemplate]);
+ }, [policies, collectionTrees]);
const copy = async () => {
if (!selectedPath) {
@@ -80,34 +75,32 @@ function DocumentCopy({ document, onSubmit }: Props) {
onSelect={selectPath}
defaultValue={document.parentDocumentId || document.collectionId || ""}
/>
- {!document.isTemplate && (
-
- {document.collectionId && (
-
-
-
- )}
- {document.publishedAt && document.childDocuments.length > 0 && (
-
-
-
- )}
-
- )}
+
+ {document.collectionId && (
+
+
+
+ )}
+ {document.publishedAt && document.childDocuments.length > 0 && (
+
+
+
+ )}
+