Hook up API
This commit is contained in:
@ -6,20 +6,47 @@ import Button from "components/Button";
|
|||||||
import CenteredContent from "components/CenteredContent";
|
import CenteredContent from "components/CenteredContent";
|
||||||
import HelpText from "components/HelpText";
|
import HelpText from "components/HelpText";
|
||||||
import PageTitle from "components/PageTitle";
|
import PageTitle from "components/PageTitle";
|
||||||
|
import VisuallyHidden from "components/VisuallyHidden";
|
||||||
import useCurrentUser from "hooks/useCurrentUser";
|
import useCurrentUser from "hooks/useCurrentUser";
|
||||||
import useStores from "hooks/useStores";
|
import useStores from "hooks/useStores";
|
||||||
|
import getDataTransferFiles from "utils/getDataTransferFiles";
|
||||||
|
|
||||||
function ImportExport() {
|
function ImportExport() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
const { ui, collections } = useStores();
|
const fileRef = React.useRef();
|
||||||
|
const { ui, collections, documents } = useStores();
|
||||||
const { showToast } = ui;
|
const { showToast } = ui;
|
||||||
const [isLoading, setLoading] = React.useState(false);
|
const [isLoading, setLoading] = React.useState(false);
|
||||||
|
const [isImporting, setImporting] = React.useState(false);
|
||||||
const [isExporting, setExporting] = React.useState(false);
|
const [isExporting, setExporting] = React.useState(false);
|
||||||
|
|
||||||
const handleImport = React.useCallback(async () => {
|
const handleFilePicked = React.useCallback(
|
||||||
// TODO
|
async (ev) => {
|
||||||
}, []);
|
const files = getDataTransferFiles(ev);
|
||||||
|
setImporting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = files[0];
|
||||||
|
await documents.batchImport(file);
|
||||||
|
showToast(t("Import completed"));
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message);
|
||||||
|
} finally {
|
||||||
|
if (fileRef.current) {
|
||||||
|
fileRef.current.value = "";
|
||||||
|
}
|
||||||
|
setImporting(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[t, documents, showToast]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleImport = React.useCallback(() => {
|
||||||
|
if (fileRef.current) {
|
||||||
|
fileRef.current.click();
|
||||||
|
}
|
||||||
|
}, [fileRef]);
|
||||||
|
|
||||||
const handleExport = React.useCallback(
|
const handleExport = React.useCallback(
|
||||||
async (ev: SyntheticEvent<>) => {
|
async (ev: SyntheticEvent<>) => {
|
||||||
@ -43,11 +70,26 @@ function ImportExport() {
|
|||||||
<h1>{t("Import")}</h1>
|
<h1>{t("Import")}</h1>
|
||||||
<HelpText>
|
<HelpText>
|
||||||
<Trans>
|
<Trans>
|
||||||
It is possible to import a zip file of folders and Markdown files.
|
It is possible to import a zip file of folders and Markdown files
|
||||||
|
previously exported from an Outline instance. Support will soon be
|
||||||
|
added for importing from other services.
|
||||||
</Trans>
|
</Trans>
|
||||||
</HelpText>
|
</HelpText>
|
||||||
<Button type="submit" onClick={handleImport} primary>
|
<VisuallyHidden>
|
||||||
{t("Import")}
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileRef}
|
||||||
|
onChange={handleFilePicked}
|
||||||
|
accept="application/zip"
|
||||||
|
/>
|
||||||
|
</VisuallyHidden>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={isImporting}
|
||||||
|
primary
|
||||||
|
>
|
||||||
|
{isImporting ? `${t("Importing")}…` : t("Import Data")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<h1>{t("Export")}</h1>
|
<h1>{t("Export")}</h1>
|
||||||
|
@ -497,6 +497,15 @@ export default class DocumentsStore extends BaseStore<Document> {
|
|||||||
return this.add(res.data);
|
return this.add(res.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@action
|
||||||
|
batchImport = async (file: File) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("type", "outline");
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
await client.post("/documents.batchImport", formData);
|
||||||
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
import = async (
|
import = async (
|
||||||
file: File,
|
file: File,
|
||||||
|
@ -5,6 +5,7 @@ import File from "formidable/lib/file";
|
|||||||
import invariant from "invariant";
|
import invariant from "invariant";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { values, keys } from "lodash";
|
import { values, keys } from "lodash";
|
||||||
|
import { InvalidRequestError } from "../errors";
|
||||||
import { Attachment, Document, Collection, User } from "../models";
|
import { Attachment, Document, Collection, User } from "../models";
|
||||||
import attachmentCreator from "./attachmentCreator";
|
import attachmentCreator from "./attachmentCreator";
|
||||||
import documentCreator from "./documentCreator";
|
import documentCreator from "./documentCreator";
|
||||||
@ -33,20 +34,31 @@ export default async function documentBatchImporter({
|
|||||||
// this is so we can use async / await a little easier
|
// this is so we can use async / await a little easier
|
||||||
let folders = [];
|
let folders = [];
|
||||||
zip.forEach(async function (path, item) {
|
zip.forEach(async function (path, item) {
|
||||||
|
// known skippable items
|
||||||
|
if (path.startsWith("__MACOSX") || path.endsWith(".DS_Store")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
folders.push([path, item]);
|
folders.push([path, item]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const [rawPath, item] of folders) {
|
||||||
|
const itemPath = rawPath.replace(/\/$/, "");
|
||||||
|
const depth = itemPath.split("/").length - 1;
|
||||||
|
|
||||||
|
if (depth === 0 && !item.dir) {
|
||||||
|
throw new InvalidRequestError(
|
||||||
|
"Root of zip file must only contain folders representing collections"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [rawPath, item] of folders) {
|
for (const [rawPath, item] of folders) {
|
||||||
const itemPath = rawPath.replace(/\/$/, "");
|
const itemPath = rawPath.replace(/\/$/, "");
|
||||||
const itemDir = path.dirname(itemPath);
|
const itemDir = path.dirname(itemPath);
|
||||||
const name = path.basename(item.name);
|
const name = path.basename(item.name);
|
||||||
const depth = itemPath.split("/").length - 1;
|
const depth = itemPath.split("/").length - 1;
|
||||||
|
|
||||||
// known skippable items
|
|
||||||
if (itemPath.startsWith("__MACOSX") || itemPath.endsWith(".DS_Store")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (depth === 0 && item.dir && name) {
|
if (depth === 0 && item.dir && name) {
|
||||||
// check if collection with name exists
|
// check if collection with name exists
|
||||||
let [collection, isCreated] = await Collection.findOrCreate({
|
let [collection, isCreated] = await Collection.findOrCreate({
|
||||||
@ -142,12 +154,17 @@ export default async function documentBatchImporter({
|
|||||||
const attachment = attachments[attachmentPath];
|
const attachment = attachments[attachmentPath];
|
||||||
|
|
||||||
for (const document of values(documents)) {
|
for (const document of values(documents)) {
|
||||||
|
// pull the collection out of the path name
|
||||||
|
const pathParts = attachmentPath.split("/");
|
||||||
|
const normalizedAttachmentPath = pathParts.splice(1).join("/");
|
||||||
|
|
||||||
document.text = document.text
|
document.text = document.text
|
||||||
.replace(attachmentPath, attachment.redirectUrl)
|
.replace(attachmentPath, attachment.redirectUrl)
|
||||||
.replace(`/${attachmentPath}`, attachment.redirectUrl);
|
.replace(normalizedAttachmentPath, attachment.redirectUrl)
|
||||||
|
.replace(`/${normalizedAttachmentPath}`, attachment.redirectUrl);
|
||||||
|
|
||||||
// does nothing if the document text is unchanged
|
// does nothing if the document text is unchanged
|
||||||
await document.save();
|
await document.save({ fields: ["text"] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,6 +275,16 @@
|
|||||||
"Use the <1>{{meta}}+K</1> shortcut to search from anywhere in your knowledge base": "Use the <1>{{meta}}+K</1> shortcut to search from anywhere in your knowledge base",
|
"Use the <1>{{meta}}+K</1> shortcut to search from anywhere in your knowledge base": "Use the <1>{{meta}}+K</1> shortcut to search from anywhere in your knowledge base",
|
||||||
"No documents found for your search filters. <1></1>Create a new document?": "No documents found for your search filters. <1></1>Create a new document?",
|
"No documents found for your search filters. <1></1>Create a new document?": "No documents found for your search filters. <1></1>Create a new document?",
|
||||||
"Clear filters": "Clear filters",
|
"Clear filters": "Clear filters",
|
||||||
|
"Import completed": "Import completed",
|
||||||
|
"Export in progress…": "Export in progress…",
|
||||||
|
"Import": "Import",
|
||||||
|
"It is possible to import a zip file of folders and Markdown files previously exported from an Outline instance. We’ll soon add support for importing from other services.": "It is possible to import a zip file of folders and Markdown files previously exported from an Outline instance. We’ll soon add support for importing from other services.",
|
||||||
|
"Importing": "Importing",
|
||||||
|
"Import Data": "Import Data",
|
||||||
|
"A full export might take some time, consider exporting a single document or collection if possible. We’ll put together a zip of all your documents in Markdown format and email it to <2>{{userEmail}}</2>.": "A full export might take some time, consider exporting a single document or collection if possible. We’ll put together a zip of all your documents in Markdown format and email it to <2>{{userEmail}}</2>.",
|
||||||
|
"Export Requested": "Export Requested",
|
||||||
|
"Requesting Export": "Requesting Export",
|
||||||
|
"Export Data": "Export Data",
|
||||||
"Profile saved": "Profile saved",
|
"Profile saved": "Profile saved",
|
||||||
"Profile picture updated": "Profile picture updated",
|
"Profile picture updated": "Profile picture updated",
|
||||||
"Unable to upload new profile picture": "Unable to upload new profile picture",
|
"Unable to upload new profile picture": "Unable to upload new profile picture",
|
||||||
|
Reference in New Issue
Block a user