From d02d3cb55d9406a9a5485b1bd2b0d647e959949b Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 29 Jan 2023 10:24:44 -0800 Subject: [PATCH] feat: Add import/export of documents as JSON (#4621) * feat: Add export of documents as JSON * Rename, add structured collection description * stash * ui * Add entity creation data to JSON archive * Import JSON UI plumbing * stash * Messy, but working * tsc * tsc --- app/components/ExportDialog.tsx | 108 +++++------ app/components/Icons/MarkdownIcon.tsx | 2 + app/components/Text.ts | 7 + app/scenes/Settings/Import.tsx | 32 +++- .../components/FileOperationListItem.tsx | 5 +- .../Settings/components/ImportJSONDialog.tsx | 41 +++++ ...ineDialog.tsx => ImportMarkdownDialog.tsx} | 7 +- .../components/ImportNotionDialog.tsx | 6 +- app/types.ts | 5 - server/commands/documentCreator.ts | 25 ++- server/models/Collection.ts | 4 +- .../processors/FileOperationsProcessor.ts | 12 ++ server/queues/tasks/ExportJSONTask.ts | 143 +++++++++++++++ server/queues/tasks/ExportTask.ts | 7 +- server/queues/tasks/ImportJSONTask.ts | 171 ++++++++++++++++++ server/queues/tasks/ImportMarkdownZipTask.ts | 8 +- server/queues/tasks/ImportNotionTask.ts | 13 +- server/queues/tasks/ImportTask.ts | 75 +++++++- server/types.ts | 64 ++++++- shared/editor/lib/markdown/serializer.ts | 2 +- shared/i18n/locales/en_US/translation.json | 14 +- shared/types.ts | 8 +- yarn.lock | 9 +- 23 files changed, 649 insertions(+), 119 deletions(-) create mode 100644 app/scenes/Settings/components/ImportJSONDialog.tsx rename app/scenes/Settings/components/{ImportOutlineDialog.tsx => ImportMarkdownDialog.tsx} (86%) create mode 100644 server/queues/tasks/ExportJSONTask.ts create mode 100644 server/queues/tasks/ImportJSONTask.ts diff --git a/app/components/ExportDialog.tsx b/app/components/ExportDialog.tsx index 8de85d491..061afb010 100644 --- a/app/components/ExportDialog.tsx +++ b/app/components/ExportDialog.tsx @@ -1,5 +1,4 @@ import { observer } from "mobx-react"; -import { CodeIcon } from "outline-icons"; import * as React from "react"; import { Trans, useTranslation } from "react-i18next"; import styled from "styled-components"; @@ -7,8 +6,8 @@ import { FileOperationFormat } from "@shared/types"; import Collection from "~/models/Collection"; import ConfirmationDialog from "~/components/ConfirmationDialog"; import Flex from "~/components/Flex"; -import MarkdownIcon from "~/components/Icons/MarkdownIcon"; import Text from "~/components/Text"; +import env from "~/env"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; @@ -24,6 +23,7 @@ function ExportDialog({ collection, onSubmit }: Props) { const { showToast } = useToasts(); const { collections, notificationSettings } = useStores(); const { t } = useTranslation(); + const appName = env.APP_NAME; React.useEffect(() => { notificationSettings.fetchPage({}); @@ -46,6 +46,33 @@ function ExportDialog({ collection, onSubmit }: Props) { showToast(t("Export started"), { type: "success" }); }; + const items = [ + { + title: "Markdown", + description: t( + "A ZIP file containing the images, and documents in the Markdown format." + ), + value: FileOperationFormat.MarkdownZip, + }, + { + title: "HTML", + description: t( + "A ZIP file containing the images, and documents as HTML files." + ), + value: FileOperationFormat.HTMLZip, + }, + { + title: "JSON", + description: t( + "Structured data that can be used to transfer data to another compatible {{ appName }} instance.", + { + appName, + } + ), + value: FileOperationFormat.JSON, + }, + ]; + return ( {collection && ( @@ -64,63 +91,28 @@ function ExportDialog({ collection, onSubmit }: Props) { )} - - + {items.map((item) => ( + + ))} ); } -const Format = styled.div` - display: flex; - flex-direction: column; - align-items: center; - flex-shrink: 0; - background: ${(props) => props.theme.secondaryBackground}; - border-radius: 6px; - width: 25%; - font-weight: 500; - font-size: 14px; - text-align: center; - padding: 10px 8px; - cursor: var(--pointer); -`; - const Option = styled.label` display: flex; align-items: center; @@ -131,12 +123,4 @@ const Option = styled.label` } `; -const Input = styled.input` - display: none; - - &:checked + ${Format} { - box-shadow: inset 0 0 0 2px ${(props) => props.theme.inputBorderFocused}; - } -`; - export default observer(ExportDialog); diff --git a/app/components/Icons/MarkdownIcon.tsx b/app/components/Icons/MarkdownIcon.tsx index f77bdb087..bfe6f7131 100644 --- a/app/components/Icons/MarkdownIcon.tsx +++ b/app/components/Icons/MarkdownIcon.tsx @@ -10,6 +10,7 @@ type Props = { export default function MarkdownIcon({ size = 24, color = "currentColor", + ...rest }: Props) { return ( ` : props.size === "xsmall" ? "13px" : "inherit"}; + font-weight: ${(props) => + props.weight === "bold" + ? "bold" + : props.weight === "normal" + ? "normal" + : "inherit"}; white-space: normal; user-select: none; `; diff --git a/app/scenes/Settings/Import.tsx b/app/scenes/Settings/Import.tsx index 12a18e4b5..86e0e85ec 100644 --- a/app/scenes/Settings/Import.tsx +++ b/app/scenes/Settings/Import.tsx @@ -8,6 +8,7 @@ import FileOperation from "~/models/FileOperation"; import Button from "~/components/Button"; import Heading from "~/components/Heading"; import MarkdownIcon from "~/components/Icons/MarkdownIcon"; +import OutlineIcon from "~/components/Icons/OutlineIcon"; import Item from "~/components/List/Item"; import PaginatedList from "~/components/PaginatedList"; import Scene from "~/components/Scene"; @@ -15,8 +16,9 @@ import Text from "~/components/Text"; import env from "~/env"; import useStores from "~/hooks/useStores"; import FileOperationListItem from "./components/FileOperationListItem"; +import ImportJSONDialog from "./components/ImportJSONDialog"; +import ImportMarkdownDialog from "./components/ImportMarkdownDialog"; import ImportNotionDialog from "./components/ImportNotionDialog"; -import ImportOutlineDialog from "./components/ImportOutlineDialog"; function Import() { const { t } = useTranslation(); @@ -50,7 +52,33 @@ function Import() { dialogs.openModal({ title: t("Import data"), isCentered: true, - content: , + content: , + }); + }} + neutral + > + {t("Import")}… + + } + /> + } + title="JSON" + subtitle={t( + "Import a JSON data file exported from another {{ appName }} instance", + { + appName, + } + )} + actions={ + {{collectionName}} may take some time.": "Exporting the collection {{collectionName}} may take some time.", "You will receive an email when it's complete.": "You will receive an email when it's complete.", - "A ZIP file containing the images, and documents in the Markdown format.": "A ZIP file containing the images, and documents in the Markdown format.", - "A ZIP file containing the images, and documents as HTML files.": "A ZIP file containing the images, and documents as HTML files.", "{{ count }} member": "{{ count }} member", "{{ count }} member_plural": "{{ count }} members", "Group members": "Group members", @@ -646,12 +647,14 @@ "All collections": "All collections", "{{userName}} requested": "{{userName}} requested", "Upload": "Upload", + "Drag and drop the zip file from the JSON export option in {{appName}}, or click to upload": "Drag and drop the zip file from the JSON export option in {{appName}}, or click to upload", + "How does this work?": "How does this work?", + "You can import a zip file that was previously exported from the JSON option in another instance. In {{ appName }}, open Export in the Settings sidebar and click on Export Data.": "You can import a zip file that was previously exported from the JSON option in another instance. In {{ appName }}, open Export in the Settings sidebar and click on Export Data.", + "Drag and drop the zip file from the Markdown export option in {{appName}}, or click to upload": "Drag and drop the zip file from the Markdown export option in {{appName}}, or click to upload", + "You can import a zip file that was previously exported from an Outline installation – collections, documents, and images will be imported. In Outline, open Export in the Settings sidebar and click on Export Data.": "You can import a zip file that was previously exported from an Outline installation – collections, documents, and images will be imported. In Outline, open Export in the Settings sidebar and click on Export Data.", "Drag and drop the zip file from Notion's HTML export option, or click to upload": "Drag and drop the zip file from Notion's HTML export option, or click to upload", "Where do I find the file?": "Where do I find the file?", "In Notion, click Settings & Members in the left sidebar and open Settings. Look for the Export section, and click Export all workspace content. Choose HTML as the format for the best data compatability.": "In Notion, click Settings & Members in the left sidebar and open Settings. Look for the Export section, and click Export all workspace content. Choose HTML as the format for the best data compatability.", - "Drag and drop the zip file from the Markdown export option in {{appName}}, or click to upload": "Drag and drop the zip file from the Markdown export option in {{appName}}, or click to upload", - "How does this work?": "How does this work?", - "You can import a zip file that was previously exported from an Outline installation – collections, documents, and images will be imported. In Outline, open Export in the Settings sidebar and click on Export Data.": "You can import a zip file that was previously exported from an Outline installation – collections, documents, and images will be imported. In Outline, open Export in the Settings sidebar and click on Export Data.", "Last active": "Last active", "Suspended": "Suspended", "Shared": "Shared", @@ -721,6 +724,7 @@ "Quickly transfer your existing documents, pages, and files from other tools and services into {{appName}}. You can also drag and drop any HTML, Markdown, and text documents directly into Collections in the app.": "Quickly transfer your existing documents, pages, and files from other tools and services into {{appName}}. You can also drag and drop any HTML, Markdown, and text documents directly into Collections in the app.", "Import a zip file of Markdown documents (exported from version 0.67.0 or earlier)": "Import a zip file of Markdown documents (exported from version 0.67.0 or earlier)", "Import data": "Import data", + "Import a JSON data file exported from another {{ appName }} instance": "Import a JSON data file exported from another {{ appName }} instance", "Import pages exported from Notion": "Import pages exported from Notion", "Import pages from a Confluence instance": "Import pages from a Confluence instance", "Enterprise": "Enterprise", diff --git a/shared/types.ts b/shared/types.ts index a0a29c008..699e7c67b 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -14,9 +14,10 @@ export enum ExportContentType { } export enum FileOperationFormat { + JSON = "json", MarkdownZip = "outline-markdown", HTMLZip = "html", - PDFZip = "pdf", + PDF = "pdf", Notion = "notion", } @@ -134,3 +135,8 @@ export type NavigationNode = { parent?: NavigationNode | null; depth?: number; }; + +export type CollectionSort = { + field: string; + direction: "asc" | "desc"; +}; diff --git a/yarn.lock b/yarn.lock index 67d407ca7..831140d59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13693,14 +13693,7 @@ rw@1: resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= -rxjs@^7.0.0: - version "7.5.6" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc" - integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw== - dependencies: - tslib "^2.1.0" - -rxjs@^7.8.0: +rxjs@^7.0.0, rxjs@^7.8.0: version "7.8.0" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==