This repository has been archived on 2022-08-14. You can view files and clone it, but cannot push or open issues or pull requests.
outline/app/scenes/Settings/ImportExport.js

247 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @flow
import invariant from "invariant";
import { observer } from "mobx-react";
import { CollectionIcon, DocumentIcon } from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import { parseOutlineExport } from "shared/utils/zip";
import Button from "components/Button";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import Notice from "components/Notice";
import PaginatedList from "components/PaginatedList";
import Scene from "components/Scene";
import Subheading from "components/Subheading";
import FileOperationListItem from "./components/FileOperationListItem";
import useCurrentUser from "hooks/useCurrentUser";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
import getDataTransferFiles from "utils/getDataTransferFiles";
import { uploadFile } from "utils/uploadFile";
function ImportExport() {
const { t } = useTranslation();
const user = useCurrentUser();
const fileRef = React.useRef();
const { fileOperations, collections } = useStores();
const { showToast } = useToasts();
const [isLoading, setLoading] = React.useState(false);
const [isImporting, setImporting] = React.useState(false);
const [isImported, setImported] = React.useState(false);
const [isExporting, setExporting] = React.useState(false);
const [file, setFile] = React.useState();
const [importDetails, setImportDetails] = React.useState();
const handleImport = React.useCallback(
async (ev) => {
setImported(undefined);
setImporting(true);
try {
invariant(file, "File must exist to upload");
const attachment = await uploadFile(file, {
name: file.name,
});
await collections.import(attachment.id);
showToast(t("Import started"));
setImported(true);
} catch (err) {
showToast(err.message);
} finally {
if (fileRef.current) {
fileRef.current.value = "";
}
setImporting(false);
setFile(undefined);
setImportDetails(undefined);
}
},
[t, file, collections, showToast]
);
const handleFilePicked = React.useCallback(async (ev) => {
ev.preventDefault();
const files = getDataTransferFiles(ev);
const file = files[0];
setFile(file);
try {
setImportDetails(await parseOutlineExport(file));
} catch (err) {
setImportDetails([]);
}
}, []);
const handlePickFile = React.useCallback(
(ev) => {
ev.preventDefault();
if (fileRef.current) {
fileRef.current.click();
}
},
[fileRef]
);
const handleExport = React.useCallback(
async (ev: SyntheticEvent<>) => {
ev.preventDefault();
setLoading(true);
try {
await collections.export();
setExporting(true);
showToast(t("Export in progress…"));
} finally {
setLoading(false);
}
},
[t, collections, showToast]
);
const hasCollections = importDetails
? !!importDetails.filter((detail) => detail.type === "collection").length
: false;
const hasDocuments = importDetails
? !!importDetails.filter((detail) => detail.type === "document").length
: false;
const isImportable = hasCollections && hasDocuments;
return (
<Scene
title={`${t("Import")} / ${t("Export")}`}
icon={<DocumentIcon color="currentColor" />}
>
<Heading>{t("Import")}</Heading>
<HelpText>
<Trans>
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>
</HelpText>
<VisuallyHidden>
<input
type="file"
ref={fileRef}
onChange={handleFilePicked}
accept="application/zip"
/>
</VisuallyHidden>
{isImported && (
<Notice>
<Trans>
Your file has been uploaded and the import is currently being
processed, you can safely leave this page while it completes.
</Trans>
</Notice>
)}
{file && !isImportable && (
<ImportPreview>
<Trans
defaults="Sorry, the file <em>{{ fileName }}</em> is missing valid collections or documents."
values={{ fileName: file.name }}
components={{ em: <strong /> }}
/>
</ImportPreview>
)}
{file && importDetails && isImportable ? (
<>
<ImportPreview as="div">
<Trans
defaults="<em>{{ fileName }}</em> looks good, the following collections and their documents will be imported:"
values={{ fileName: file.name }}
components={{ em: <strong /> }}
/>
<List>
{importDetails
.filter((detail) => detail.type === "collection")
.map((detail) => (
<ImportPreviewItem key={detail.path}>
<CollectionIcon />
<CollectionName>{detail.name}</CollectionName>
</ImportPreviewItem>
))}
</List>
</ImportPreview>
<Button
type="submit"
onClick={handleImport}
disabled={isImporting}
primary
>
{isImporting ? `${t("Uploading")}` : t("Confirm & Import")}
</Button>
</>
) : (
<Button type="submit" onClick={handlePickFile} primary>
{t("Choose File")}
</Button>
)}
<Heading>{t("Export")}</Heading>
<HelpText>
<Trans
defaults="A full export might take some time, consider exporting a single document or collection. The exported data is a zip of your documents in Markdown format. You may leave this page once the export has started we will email a link to <em>{{ userEmail }}</em> when it's complete."
values={{ userEmail: user.email }}
components={{ em: <strong /> }}
/>
</HelpText>
<Button
type="submit"
onClick={handleExport}
disabled={isLoading || isExporting}
primary
>
{isExporting
? t("Export Requested")
: isLoading
? `${t("Requesting Export")}`
: t("Export Data")}
</Button>
<br />
<br />
<PaginatedList
items={fileOperations.orderedDataExports}
fetch={fileOperations.fetchPage}
options={{ type: "export" }}
heading={
<Subheading>
<Trans>Recent exports</Trans>
</Subheading>
}
renderItem={(item) => (
<FileOperationListItem
key={item.id + item.state}
fileOperation={item}
/>
)}
/>
</Scene>
);
}
const List = styled.ul`
padding: 0;
margin: 8px 0 0;
`;
const ImportPreview = styled(Notice)`
margin-bottom: 16px;
`;
const ImportPreviewItem = styled.li`
display: flex;
align-items: center;
list-style: none;
`;
const CollectionName = styled.span`
font-weight: 500;
margin-left: 4px;
`;
export default observer(ImportExport);