feat: Bulk HTML export for collection
This commit is contained in:
@@ -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
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) => {
|
||||
})}
|
||||
|
||||
<Time dateTime={fileOperation.createdAt} addSuffix shorten />
|
||||
{format ? <> • {format}</> : ""}
|
||||
• {fileOperation.sizeInMB}
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
fileOperation.state === "complete" && handleDelete ? (
|
||||
fileOperation.state === FileOperationState.Complete && handleDelete ? (
|
||||
<Action>
|
||||
<FileOperationMenu
|
||||
id={fileOperation.id}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user