From 00ba65f3ef4574921e74e055e0015ac84328d757 Mon Sep 17 00:00:00 2001 From: Saumya Pandey Date: Sun, 29 Aug 2021 02:57:07 +0530 Subject: [PATCH] fix: Refactor collection exports to not send email attachment (#2460) Co-authored-by: Tom Moor --- app/components/Button.js | 4 +- app/components/SocketProvider.js | 21 +- app/menus/CollectionMenu.js | 16 +- app/models/Collection.js | 6 +- app/models/FileOperation.js | 27 ++ app/scenes/CollectionExport.js | 10 +- app/scenes/Settings/ImportExport.js | 26 +- .../components/FileOperationListItem.js | 61 ++++ app/stores/FileOperationsStore.js | 27 ++ app/stores/RootStore.js | 4 + server/api/attachments.js | 4 +- server/api/collections.js | 86 +++--- server/api/collections.test.js | 60 ++-- server/api/fileOperations.js | 99 ++++++ server/api/fileOperations.test.js | 283 ++++++++++++++++++ server/api/index.js | 2 + server/api/utils.js | 22 +- server/api/utils.test.js | 67 ++++- server/emails/ExportEmail.js | 36 --- server/emails/ExportFailureEmail.js | 47 +++ server/emails/ExportSuccessEmail.js | 53 ++++ server/exporter.js | 162 ++++++++-- server/mailer.js | 36 ++- .../20210730210120-add-fileOperations.js | 65 ++++ server/models/Event.js | 1 + server/models/FileOperation.js | 72 +++++ server/models/index.js | 3 + server/policies/collection.js | 2 +- server/policies/collection.test.js | 6 - server/presenters/document.js | 4 +- server/presenters/fileOperation.js | 15 + server/presenters/index.js | 2 + server/queues/processors/websockets.js | 7 + server/test/factories.js | 26 ++ server/types.js | 26 ++ server/utils/s3.js | 9 +- server/utils/zip.js | 11 - shared/i18n/locales/en_US/translation.json | 11 +- 38 files changed, 1252 insertions(+), 167 deletions(-) create mode 100644 app/models/FileOperation.js create mode 100644 app/scenes/Settings/components/FileOperationListItem.js create mode 100644 app/stores/FileOperationsStore.js create mode 100644 server/api/fileOperations.js create mode 100644 server/api/fileOperations.test.js delete mode 100644 server/emails/ExportEmail.js create mode 100644 server/emails/ExportFailureEmail.js create mode 100644 server/emails/ExportSuccessEmail.js create mode 100644 server/migrations/20210730210120-add-fileOperations.js create mode 100644 server/models/FileOperation.js create mode 100644 server/presenters/fileOperation.js diff --git a/app/components/Button.js b/app/components/Button.js index fc3b4f64..67ba45f5 100644 --- a/app/components/Button.js +++ b/app/components/Button.js @@ -128,11 +128,11 @@ export type Props = {| fullwidth?: boolean, autoFocus?: boolean, style?: Object, - as?: React.ComponentType, + as?: React.ComponentType | string, to?: string, onClick?: (event: SyntheticEvent<>) => mixed, borderOnHover?: boolean, - + href?: string, "data-on"?: string, "data-event-category"?: string, "data-event-action"?: string, diff --git a/app/components/SocketProvider.js b/app/components/SocketProvider.js index 4ee7b212..9135e14b 100644 --- a/app/components/SocketProvider.js +++ b/app/components/SocketProvider.js @@ -8,6 +8,7 @@ import AuthStore from "stores/AuthStore"; import CollectionsStore from "stores/CollectionsStore"; import DocumentPresenceStore from "stores/DocumentPresenceStore"; import DocumentsStore from "stores/DocumentsStore"; +import FileOperationsStore from "stores/FileOperationsStore"; import GroupsStore from "stores/GroupsStore"; import MembershipsStore from "stores/MembershipsStore"; import PoliciesStore from "stores/PoliciesStore"; @@ -28,6 +29,7 @@ type Props = { views: ViewsStore, auth: AuthStore, toasts: ToastsStore, + fileOperations: FileOperationsStore, }; @observer @@ -80,6 +82,7 @@ class SocketProvider extends React.Component { policies, presence, views, + fileOperations, } = this.props; if (!auth.token) return; @@ -287,6 +290,21 @@ class SocketProvider extends React.Component { } }); + this.socket.on("fileOperations.update", async (event) => { + const user = auth.user; + let collection = null; + + if (event.collectionId) + collection = await collections.fetch(event.collectionId); + if (user) { + fileOperations.add({ + ...event, + user, + collection, + }); + } + }); + // received a message from the API server that we should request // to join a specific room. Forward that to the ws server. this.socket.on("join", (event) => { @@ -345,5 +363,6 @@ export default inject( "memberships", "presence", "policies", - "views" + "views", + "fileOperations" )(SocketProvider); diff --git a/app/menus/CollectionMenu.js b/app/menus/CollectionMenu.js index f2135503..006a8956 100644 --- a/app/menus/CollectionMenu.js +++ b/app/menus/CollectionMenu.js @@ -22,6 +22,7 @@ import ContextMenu from "components/ContextMenu"; import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton"; import Template from "components/ContextMenu/Template"; import Modal from "components/Modal"; +import useCurrentTeam from "hooks/useCurrentTeam"; import useStores from "hooks/useStores"; import useToasts from "hooks/useToasts"; import getDataTransferFiles from "utils/getDataTransferFiles"; @@ -46,6 +47,7 @@ function CollectionMenu({ }: Props) { const menu = useMenuState({ modal, placement }); const [renderModals, setRenderModals] = React.useState(false); + const team = useCurrentTeam(); const { documents, policies } = useStores(); const { showToast } = useToasts(); const { t } = useTranslation(); @@ -120,6 +122,8 @@ function CollectionMenu({ ); const can = policies.abilities(collection.id); + const canUserInTeam = policies.abilities(team.id); + const items = React.useMemo( () => [ { @@ -151,7 +155,7 @@ function CollectionMenu({ }, { title: `${t("Export")}…`, - visible: !!(collection && can.export), + visible: !!(collection && canUserInTeam.export), onClick: () => setShowCollectionExport(true), icon: , }, @@ -165,7 +169,15 @@ function CollectionMenu({ icon: , }, ], - [can, collection, handleNewDocument, handleImportDocument, t] + [ + t, + can.update, + can.delete, + handleNewDocument, + handleImportDocument, + collection, + canUserInTeam.export, + ] ); if (!items.length) { diff --git a/app/models/Collection.js b/app/models/Collection.js index c9b73b69..23d19686 100644 --- a/app/models/Collection.js +++ b/app/models/Collection.js @@ -124,10 +124,6 @@ export default class Collection extends BaseModel { }; export = () => { - return client.get( - "/collections.export", - { id: this.id }, - { download: true } - ); + return client.get("/collections.export", { id: this.id }); }; } diff --git a/app/models/FileOperation.js b/app/models/FileOperation.js new file mode 100644 index 00000000..5bae1993 --- /dev/null +++ b/app/models/FileOperation.js @@ -0,0 +1,27 @@ +// @flow +import { computed } from "mobx"; +import BaseModal from "./BaseModel"; +import Collection from "./Collection"; +import User from "./User"; + +class FileOperation extends BaseModal { + id: string; + state: string; + collection: ?Collection; + size: number; + type: string; + user: User; + createdAt: string; + + @computed + get sizeInMB(): string { + const inKB = this.size / 1024; + if (inKB < 1024) { + return inKB.toFixed(2) + "KB"; + } + + return (inKB / 1024).toFixed(2) + "MB"; + } +} + +export default FileOperation; diff --git a/app/scenes/CollectionExport.js b/app/scenes/CollectionExport.js index 7fdd44a9..9f303ba3 100644 --- a/app/scenes/CollectionExport.js +++ b/app/scenes/CollectionExport.js @@ -6,7 +6,7 @@ import Collection from "models/Collection"; import Button from "components/Button"; import Flex from "components/Flex"; import HelpText from "components/HelpText"; - +import useToasts from "hooks/useToasts"; type Props = { collection: Collection, onSubmit: () => void, @@ -15,6 +15,7 @@ type Props = { function CollectionExport({ collection, onSubmit }: Props) { const [isLoading, setIsLoading] = React.useState(); const { t } = useTranslation(); + const { showToast } = useToasts(); const handleSubmit = React.useCallback( async (ev: SyntheticEvent<>) => { @@ -23,9 +24,12 @@ function CollectionExport({ collection, onSubmit }: Props) { setIsLoading(true); await collection.export(); setIsLoading(false); + showToast( + t("Export started, you will receive an email when it’s complete.") + ); onSubmit(); }, - [collection, onSubmit] + [collection, onSubmit, showToast, t] ); return ( @@ -33,7 +37,7 @@ function CollectionExport({ collection, onSubmit }: Props) {
}} /> diff --git a/app/scenes/Settings/ImportExport.js b/app/scenes/Settings/ImportExport.js index f56ceba5..27c01155 100644 --- a/app/scenes/Settings/ImportExport.js +++ b/app/scenes/Settings/ImportExport.js @@ -11,7 +11,10 @@ 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"; @@ -22,7 +25,7 @@ function ImportExport() { const { t } = useTranslation(); const user = useCurrentUser(); const fileRef = React.useRef(); - const { collections } = useStores(); + const { fileOperations, collections } = useStores(); const { showToast } = useToasts(); const [isLoading, setLoading] = React.useState(false); const [isImporting, setImporting] = React.useState(false); @@ -178,11 +181,10 @@ function ImportExport() { {t("Choose File")}… )} - {t("Export")} }} /> @@ -199,6 +201,24 @@ function ImportExport() { ? `${t("Requesting Export")}…` : t("Export Data")} +
+
+ + Recent exports + + } + renderItem={(item) => ( + + )} + /> ); } diff --git a/app/scenes/Settings/components/FileOperationListItem.js b/app/scenes/Settings/components/FileOperationListItem.js new file mode 100644 index 00000000..2b426b5b --- /dev/null +++ b/app/scenes/Settings/components/FileOperationListItem.js @@ -0,0 +1,61 @@ +// @flow +import * as React from "react"; +import { useTranslation } from "react-i18next"; +import FileOperation from "models/FileOperation"; +import Button from "components/Button"; +import ListItem from "components/List/Item"; +import Time from "components/Time"; + +type Props = {| + fileOperation: FileOperation, +|}; + +const FileOperationListItem = ({ fileOperation }: Props) => { + const { t } = useTranslation(); + + const stateMapping = { + creating: t("Processing"), + expired: t("Expired"), + uploading: t("Processing"), + error: t("Error"), + }; + + return ( + + {fileOperation.state !== "complete" && ( + <>{stateMapping[fileOperation.state]} •  + )} + {t(`{{userName}} requested`, { + userName: + fileOperation.id === fileOperation.user.id + ? t("You") + : fileOperation.user.name, + })} +   +