feat: Bulk HTML export for collection

This commit is contained in:
Tom Moor
2022-12-30 20:13:29 -05:00
parent 1328162921
commit 7a1e6a1b73
10 changed files with 74 additions and 101 deletions

View File

@@ -4,23 +4,31 @@ import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
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/Markdown";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
type Props = {
collection?: Collection;
onSubmit: () => void;
};
function ExportDialog({ onSubmit }: Props) {
function ExportDialog({ collection, onSubmit }: Props) {
const [format, setFormat] = React.useState<FileOperationFormat>(
FileOperationFormat.MarkdownZip
);
const { collections } = useStores();
const { showToast } = useToasts();
const { collections, notificationSettings } = useStores();
const { t } = useTranslation();
React.useEffect(() => {
notificationSettings.fetchPage({});
}, [notificationSettings]);
const handleFormatChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setFormat(ev.target.value as FileOperationFormat);
@@ -28,13 +36,33 @@ function ExportDialog({ onSubmit }: Props) {
[]
);
const handleSubmit = React.useCallback(async () => {
await collections.export(format);
const handleSubmit = async () => {
if (collection) {
await collection.export(format);
} else {
await collections.export(format);
}
onSubmit();
}, [collections, format, onSubmit]);
showToast(t("Export started"), { type: "success" });
};
return (
<ConfirmationDialog onSubmit={handleSubmit} submitText={t("Export")}>
{collection && (
<Text>
<Trans
defaults="Exporting the collection <em>{{collectionName}}</em> may take some time."
values={{
collectionName: collection.name,
}}
components={{
em: <strong />,
}}
/>{" "}
{notificationSettings.getByEvent("emails.export_completed") &&
t("You will receive an email when it's complete.")}
</Text>
)}
<Flex gap={12} column>
<Option>
<Input

View File

@@ -16,11 +16,11 @@ import { useMenuState, MenuButton, MenuButtonHTMLProps } from "reakit/Menu";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import { getEventFiles } from "@shared/utils/files";
import Collection from "~/models/Collection";
import CollectionExport from "~/scenes/CollectionExport";
import CollectionDeleteDialog from "~/components/CollectionDeleteDialog";
import ContextMenu, { Placement } from "~/components/ContextMenu";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import Template from "~/components/ContextMenu/Template";
import ExportDialog from "~/components/ExportDialog";
import { actionToMenuItem } from "~/actions";
import {
editCollection,
@@ -67,7 +67,7 @@ function CollectionMenu({
title: t("Export collection"),
isCentered: true,
content: (
<CollectionExport
<ExportDialog
collection={collection}
onSubmit={dialogs.closeAllModals}
/>

View File

@@ -1,6 +1,6 @@
import { trim } from "lodash";
import { action, computed, observable } from "mobx";
import { CollectionPermission } from "@shared/types";
import { CollectionPermission, FileOperationFormat } from "@shared/types";
import { sortNavigationNodes } from "@shared/utils/collections";
import CollectionsStore from "~/stores/CollectionsStore";
import Document from "~/models/Document";
@@ -215,9 +215,10 @@ export default class Collection extends ParanoidModel {
return this.store.unstar(this);
};
export = () => {
export = (format: FileOperationFormat) => {
return client.post("/collections.export", {
id: this.id,
format,
});
};
}

View File

@@ -1,65 +0,0 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import Collection from "~/models/Collection";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import useToasts from "~/hooks/useToasts";
type Props = {
collection: Collection;
onSubmit: () => void;
};
function CollectionExport({ collection, onSubmit }: Props) {
const [isLoading, setIsLoading] = React.useState(false);
const { t } = useTranslation();
const { showToast } = useToasts();
const handleSubmit = React.useCallback(
async (ev: React.SyntheticEvent) => {
ev.preventDefault();
setIsLoading(true);
await collection.export();
setIsLoading(false);
showToast(
t(
"Export started. If you have notifications enabled, you will receive an email when it's complete."
)
);
onSubmit();
},
[collection, onSubmit, showToast, t]
);
return (
<Flex column>
<form onSubmit={handleSubmit}>
<Text type="secondary">
<Trans
defaults="Exporting the collection <em>{{collectionName}}</em> may take some time."
values={{
collectionName: collection.name,
}}
components={{
em: <strong />,
}}
/>
</Text>
<Text type="secondary">
<Trans>
Your documents will be a zip of folders with files in Markdown
format. Please visit the Export section in Settings to get the zip.
</Trans>
</Text>
<Button type="submit" disabled={isLoading}>
{isLoading ? `${t("Exporting")}` : t("Export collection")}
</Button>
</form>
</Flex>
);
}
export default observer(CollectionExport);

View File

@@ -11,7 +11,7 @@ import Text from "~/components/Text";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import ExportDialog from "./components/ExportDialog";
import ExportDialog from "../../components/ExportDialog";
import FileOperationListItem from "./components/FileOperationListItem";
function Export() {

View File

@@ -3,7 +3,11 @@ import { ArchiveIcon, DoneIcon, WarningIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useTheme } from "styled-components";
import { FileOperationFormat, FileOperationType } from "@shared/types";
import {
FileOperationFormat,
FileOperationState,
FileOperationType,
} from "@shared/types";
import FileOperation from "~/models/FileOperation";
import { Action } from "~/components/Actions";
import ListItem from "~/components/List/Item";
@@ -22,34 +26,33 @@ const FileOperationListItem = ({ fileOperation, handleDelete }: Props) => {
const user = useCurrentUser();
const theme = useTheme();
const stateMapping = {
complete: t("Completed"),
creating: t("Processing"),
expired: t("Expired"),
uploading: t("Processing"),
error: t("Failed"),
[FileOperationState.Creating]: t("Processing"),
[FileOperationState.Uploading]: t("Processing"),
[FileOperationState.Expired]: t("Expired"),
[FileOperationState.Complete]: t("Completed"),
[FileOperationState.Error]: t("Failed"),
};
const iconMapping = {
creating: <Spinner />,
uploading: <Spinner />,
expired: <ArchiveIcon color={theme.textTertiary} />,
complete: <DoneIcon color={theme.primary} />,
error: <WarningIcon color={theme.danger} />,
[FileOperationState.Creating]: <Spinner />,
[FileOperationState.Uploading]: <Spinner />,
[FileOperationState.Expired]: <ArchiveIcon color={theme.textTertiary} />,
[FileOperationState.Complete]: <DoneIcon color={theme.primary} />,
[FileOperationState.Error]: <WarningIcon color={theme.danger} />,
};
const formatToReadable = {
const formatMapping = {
[FileOperationFormat.MarkdownZip]: "Markdown",
[FileOperationFormat.HTMLZip]: "HTML",
[FileOperationFormat.PDFZip]: "PDF",
};
const format = formatToReadable[fileOperation.format];
const format = formatMapping[fileOperation.format];
const title =
fileOperation.type === FileOperationType.Import ||
fileOperation.collectionId
? fileOperation.name
: t("All collections") + (format ? `${format}` : "");
: t("All collections");
return (
<ListItem
@@ -67,11 +70,12 @@ const FileOperationListItem = ({ fileOperation, handleDelete }: Props) => {
})}
&nbsp;
<Time dateTime={fileOperation.createdAt} addSuffix shorten />
{format ? <>&nbsp;&nbsp;{format}</> : ""}
&nbsp;&nbsp;{fileOperation.sizeInMB}
</>
}
actions={
fileOperation.state === "complete" && handleDelete ? (
fileOperation.state === FileOperationState.Complete && handleDelete ? (
<Action>
<FileOperationMenu
id={fileOperation.id}

View File

@@ -1,7 +1,7 @@
import invariant from "invariant";
import { concat, find, last } from "lodash";
import { computed, action } from "mobx";
import { CollectionPermission } from "@shared/types";
import { CollectionPermission, FileOperationFormat } from "@shared/types";
import Collection from "~/models/Collection";
import { NavigationNode } from "~/types";
import { client } from "~/utils/ApiClient";
@@ -215,7 +215,7 @@ export default class CollectionsStore extends BaseStore<Collection> {
this.rootStore.documents.fetchRecentlyViewed();
};
export = (format: string) => {
export = (format: FileOperationFormat) => {
return client.post("/collections.export_all", {
format,
});