diff --git a/app/components/CollectionDescription.js b/app/components/CollectionDescription.js index d8928491..9171bfbd 100644 --- a/app/components/CollectionDescription.js +++ b/app/components/CollectionDescription.js @@ -12,13 +12,15 @@ import LoadingIndicator from "components/LoadingIndicator"; import NudeButton from "components/NudeButton"; import useDebouncedCallback from "hooks/useDebouncedCallback"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = {| collection: Collection, |}; function CollectionDescription({ collection }: Props) { - const { collections, ui, policies } = useStores(); + const { collections, policies } = useStores(); + const { showToast } = useToasts(); const { t } = useTranslation(); const [isExpanded, setExpanded] = React.useState(false); const [isEditing, setEditing] = React.useState(false); @@ -53,7 +55,7 @@ function CollectionDescription({ collection }: Props) { }); setDirty(false); } catch (err) { - ui.showToast( + showToast( t("Sorry, an error occurred saving the collection", { type: "error", }) diff --git a/app/components/Editor.js b/app/components/Editor.js index eb7898c2..6360a148 100644 --- a/app/components/Editor.js +++ b/app/components/Editor.js @@ -10,6 +10,7 @@ import ErrorBoundary from "components/ErrorBoundary"; import Tooltip from "components/Tooltip"; import embeds from "../embeds"; import useMediaQuery from "hooks/useMediaQuery"; +import useToasts from "hooks/useToasts"; import { type Theme } from "types"; import { isModKey } from "utils/keyboard"; import { uploadFile } from "utils/uploadFile"; @@ -58,8 +59,9 @@ type PropsWithRef = Props & { }; function Editor(props: PropsWithRef) { - const { id, ui, shareId, history } = props; + const { id, shareId, history } = props; const { t } = useTranslation(); + const { showToast } = useToasts(); const isPrinting = useMediaQuery("print"); const onUploadImage = React.useCallback( @@ -106,11 +108,9 @@ function Editor(props: PropsWithRef) { const onShowToast = React.useCallback( (message: string) => { - if (ui) { - ui.showToast(message); - } + showToast(message); }, - [ui] + [showToast] ); const dictionary = React.useMemo(() => { diff --git a/app/components/Sidebar/components/Collections.js b/app/components/Sidebar/components/Collections.js index 3517b7a1..3e877796 100644 --- a/app/components/Sidebar/components/Collections.js +++ b/app/components/Sidebar/components/Collections.js @@ -14,6 +14,8 @@ import Header from "./Header"; import PlaceholderCollections from "./PlaceholderCollections"; import SidebarLink from "./SidebarLink"; import useCurrentTeam from "hooks/useCurrentTeam"; +import useToasts from "hooks/useToasts"; + type Props = { onCreateCollection: () => void, }; @@ -22,6 +24,7 @@ function Collections({ onCreateCollection }: Props) { const [isFetching, setFetching] = React.useState(false); const [fetchError, setFetchError] = React.useState(); const { ui, policies, documents, collections } = useStores(); + const { showToast } = useToasts(); const isPreloaded: boolean = !!collections.orderedData.length; const { t } = useTranslation(); const team = useCurrentTeam(); @@ -38,7 +41,7 @@ function Collections({ onCreateCollection }: Props) { setFetching(true); await collections.fetchPage({ limit: 100 }); } catch (error) { - ui.showToast( + showToast( t("Collections could not be loaded, please reload the app"), { type: "error", @@ -51,7 +54,7 @@ function Collections({ onCreateCollection }: Props) { } } load(); - }, [collections, isFetching, ui, fetchError, t]); + }, [collections, isFetching, showToast, fetchError, t]); const [{ isCollectionDropping }, dropToReorderCollection] = useDrop({ accept: "collection", diff --git a/app/components/Sidebar/components/DropToImport.js b/app/components/Sidebar/components/DropToImport.js index a63342cc..cb503dc8 100644 --- a/app/components/Sidebar/components/DropToImport.js +++ b/app/components/Sidebar/components/DropToImport.js @@ -7,6 +7,7 @@ import styled, { css } from "styled-components"; import LoadingIndicator from "components/LoadingIndicator"; import useImportDocument from "hooks/useImportDocument"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = {| children: React.Node, @@ -18,7 +19,8 @@ type Props = {| function DropToImport({ disabled, children, collectionId, documentId }: Props) { const { t } = useTranslation(); - const { ui, documents, policies } = useStores(); + const { documents, policies } = useStores(); + const { showToast } = useToasts(); const { handleFiles, isImporting } = useImportDocument( collectionId, documentId @@ -27,11 +29,11 @@ function DropToImport({ disabled, children, collectionId, documentId }: Props) { const can = policies.abilities(collectionId); const handleRejection = React.useCallback(() => { - ui.showToast( + showToast( t("Document not supported – try Markdown, Plain text, HTML, or Word"), { type: "error" } ); - }, [t, ui]); + }, [t, showToast]); if (disabled || !can.update) { return children; diff --git a/app/components/Sidebar/components/EditableTitle.js b/app/components/Sidebar/components/EditableTitle.js index abe3b9f0..4a96cf8d 100644 --- a/app/components/Sidebar/components/EditableTitle.js +++ b/app/components/Sidebar/components/EditableTitle.js @@ -1,7 +1,7 @@ // @flow import * as React from "react"; import styled from "styled-components"; -import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = {| onSubmit: (title: string) => Promise, @@ -13,8 +13,7 @@ function EditableTitle({ title, onSubmit, canUpdate }: Props) { const [isEditing, setIsEditing] = React.useState(false); const [originalValue, setOriginalValue] = React.useState(title); const [value, setValue] = React.useState(title); - const { ui } = useStores(); - + const { showToast } = useToasts(); React.useEffect(() => { setValue(title); }, [title]); @@ -52,13 +51,13 @@ function EditableTitle({ title, onSubmit, canUpdate }: Props) { setOriginalValue(value); } catch (error) { setValue(originalValue); - ui.showToast(error.message, { + showToast(error.message, { type: "error", }); throw error; } } - }, [ui, originalValue, value, onSubmit]); + }, [originalValue, showToast, value, onSubmit]); return ( <> diff --git a/app/components/SocketProvider.js b/app/components/SocketProvider.js index 810ac4b3..4ee7b212 100644 --- a/app/components/SocketProvider.js +++ b/app/components/SocketProvider.js @@ -11,7 +11,7 @@ import DocumentsStore from "stores/DocumentsStore"; import GroupsStore from "stores/GroupsStore"; import MembershipsStore from "stores/MembershipsStore"; import PoliciesStore from "stores/PoliciesStore"; -import UiStore from "stores/UiStore"; +import ToastsStore from "stores/ToastsStore"; import ViewsStore from "stores/ViewsStore"; import { getVisibilityListener, getPageVisible } from "utils/pageVisibility"; @@ -27,7 +27,7 @@ type Props = { policies: PoliciesStore, views: ViewsStore, auth: AuthStore, - ui: UiStore, + toasts: ToastsStore, }; @observer @@ -72,7 +72,7 @@ class SocketProvider extends React.Component { const { auth, - ui, + toasts, documents, collections, groups, @@ -113,7 +113,7 @@ class SocketProvider extends React.Component { this.socket.on("unauthorized", (err) => { this.socket.authenticated = false; - ui.showToast(err.message, { + toasts.showToast(err.message, { type: "error", }); throw err; @@ -338,7 +338,7 @@ class SocketProvider extends React.Component { export default inject( "auth", - "ui", + "toasts", "documents", "collections", "groups", diff --git a/app/components/Toasts.js b/app/components/Toasts.js index df2502fd..6ae1496b 100644 --- a/app/components/Toasts.js +++ b/app/components/Toasts.js @@ -6,15 +6,15 @@ import Toast from "components/Toast"; import useStores from "hooks/useStores"; function Toasts() { - const { ui } = useStores(); + const { toasts } = useStores(); return ( - {ui.orderedToasts.map((toast) => ( + {toasts.orderedData.map((toast) => ( ui.removeToast(toast.id)} + onRequestClose={() => toasts.hideToast(toast.id)} /> ))} diff --git a/app/hooks/useImportDocument.js b/app/hooks/useImportDocument.js index de68824b..ecc23d62 100644 --- a/app/hooks/useImportDocument.js +++ b/app/hooks/useImportDocument.js @@ -4,6 +4,7 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; let importingLock = false; @@ -11,7 +12,8 @@ export default function useImportDocument( collectionId: string, documentId?: string ): {| handleFiles: (files: File[]) => Promise, isImporting: boolean |} { - const { documents, ui } = useStores(); + const { documents } = useStores(); + const { showToast } = useToasts(); const [isImporting, setImporting] = React.useState(false); const { t } = useTranslation(); const history = useHistory(); @@ -51,7 +53,7 @@ export default function useImportDocument( } } } catch (err) { - ui.showToast(`${t("Could not import file")}. ${err.message}`, { + showToast(`${t("Could not import file")}. ${err.message}`, { type: "error", }); } finally { @@ -59,7 +61,7 @@ export default function useImportDocument( importingLock = false; } }, - [t, ui, documents, history, collectionId, documentId] + [t, documents, history, showToast, collectionId, documentId] ); return { diff --git a/app/hooks/useToasts.js b/app/hooks/useToasts.js new file mode 100644 index 00000000..e91c6f57 --- /dev/null +++ b/app/hooks/useToasts.js @@ -0,0 +1,8 @@ +// @flow +import useStores from "./useStores"; + +export default function useToasts() { + const { toasts } = useStores(); + + return { showToast: toasts.showToast, hideToast: toasts.hideToast }; +} diff --git a/app/menus/CollectionMenu.js b/app/menus/CollectionMenu.js index 393a03dc..b6cb5676 100644 --- a/app/menus/CollectionMenu.js +++ b/app/menus/CollectionMenu.js @@ -15,6 +15,7 @@ import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton"; import Template, { filterTemplateItems } from "components/ContextMenu/Template"; import Modal from "components/Modal"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; import getDataTransferFiles from "utils/getDataTransferFiles"; import { newDocumentUrl } from "utils/routeHelpers"; @@ -37,7 +38,8 @@ function CollectionMenu({ }: Props) { const menu = useMenuState({ modal, placement }); const [renderModals, setRenderModals] = React.useState(false); - const { ui, documents, policies } = useStores(); + const { documents, policies } = useStores(); + const { showToast } = useToasts(); const { t } = useTranslation(); const history = useHistory(); @@ -99,14 +101,14 @@ function CollectionMenu({ }); history.push(document.url); } catch (err) { - ui.showToast(err.message, { + showToast(err.message, { type: "error", }); throw err; } }, - [history, ui, collection.id, documents] + [history, showToast, collection.id, documents] ); const can = policies.abilities(collection.id); diff --git a/app/menus/DocumentMenu.js b/app/menus/DocumentMenu.js index 79b3d987..b6c7c50f 100644 --- a/app/menus/DocumentMenu.js +++ b/app/menus/DocumentMenu.js @@ -18,6 +18,7 @@ import Template from "components/ContextMenu/Template"; import Flex from "components/Flex"; import Modal from "components/Modal"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; import getDataTransferFiles from "utils/getDataTransferFiles"; import { documentHistoryUrl, @@ -51,7 +52,8 @@ function DocumentMenu({ onOpen, onClose, }: Props) { - const { policies, collections, ui, documents } = useStores(); + const { policies, collections, documents } = useStores(); + const { showToast } = useToasts(); const menu = useMenuState({ modal, unstable_preventOverflow: true, @@ -83,33 +85,33 @@ function DocumentMenu({ // when duplicating, go straight to the duplicated document content history.push(duped.url); - ui.showToast(t("Document duplicated"), { type: "success" }); + showToast(t("Document duplicated"), { type: "success" }); }, - [ui, t, history, document] + [t, history, showToast, document] ); const handleArchive = React.useCallback( async (ev: SyntheticEvent<>) => { await document.archive(); - ui.showToast(t("Document archived"), { type: "success" }); + showToast(t("Document archived"), { type: "success" }); }, - [ui, t, document] + [showToast, t, document] ); const handleRestore = React.useCallback( async (ev: SyntheticEvent<>, options?: { collectionId: string }) => { await document.restore(options); - ui.showToast(t("Document restored"), { type: "success" }); + showToast(t("Document restored"), { type: "success" }); }, - [ui, t, document] + [showToast, t, document] ); const handleUnpublish = React.useCallback( async (ev: SyntheticEvent<>) => { await document.unpublish(); - ui.showToast(t("Document unpublished"), { type: "success" }); + showToast(t("Document unpublished"), { type: "success" }); }, - [ui, t, document] + [showToast, t, document] ); const handlePrint = React.useCallback((ev: SyntheticEvent<>) => { @@ -181,14 +183,14 @@ function DocumentMenu({ ); history.push(importedDocument.url); } catch (err) { - ui.showToast(err.message, { + showToast(err.message, { type: "error", }); throw err; } }, - [history, ui, collection, documents, document.id] + [history, showToast, collection, documents, document.id] ); return ( diff --git a/app/menus/RevisionMenu.js b/app/menus/RevisionMenu.js index bc587295..000f8099 100644 --- a/app/menus/RevisionMenu.js +++ b/app/menus/RevisionMenu.js @@ -11,7 +11,7 @@ import MenuItem from "components/ContextMenu/MenuItem"; import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton"; import Separator from "components/ContextMenu/Separator"; import CopyToClipboard from "components/CopyToClipboard"; -import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; import { documentHistoryUrl } from "utils/routeHelpers"; type Props = {| @@ -22,7 +22,7 @@ type Props = {| |}; function RevisionMenu({ document, revision, className, iconColor }: Props) { - const { ui } = useStores(); + const { showToast } = useToasts(); const menu = useMenuState({ modal: true }); const { t } = useTranslation(); const history = useHistory(); @@ -31,15 +31,15 @@ function RevisionMenu({ document, revision, className, iconColor }: Props) { async (ev: SyntheticEvent<>) => { ev.preventDefault(); await document.restore({ revisionId: revision.id }); - ui.showToast(t("Document restored"), { type: "success" }); + showToast(t("Document restored"), { type: "success" }); history.push(document.url); }, - [history, ui, t, document, revision] + [history, showToast, t, document, revision] ); const handleCopy = React.useCallback(() => { - ui.showToast(t("Link copied"), { type: "info" }); - }, [ui, t]); + showToast(t("Link copied"), { type: "info" }); + }, [showToast, t]); const url = `${window.location.origin}${documentHistoryUrl( document, diff --git a/app/menus/ShareMenu.js b/app/menus/ShareMenu.js index d5773f49..77daa3d2 100644 --- a/app/menus/ShareMenu.js +++ b/app/menus/ShareMenu.js @@ -10,6 +10,7 @@ import MenuItem from "components/ContextMenu/MenuItem"; import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton"; import CopyToClipboard from "components/CopyToClipboard"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = { share: Share, @@ -17,7 +18,8 @@ type Props = { function ShareMenu({ share }: Props) { const menu = useMenuState({ modal: true }); - const { ui, shares, policies } = useStores(); + const { shares, policies } = useStores(); + const { showToast } = useToasts(); const { t } = useTranslation(); const history = useHistory(); const can = policies.abilities(share.id); @@ -36,17 +38,17 @@ function ShareMenu({ share }: Props) { try { await shares.revoke(share); - ui.showToast(t("Share link revoked"), { type: "info" }); + showToast(t("Share link revoked"), { type: "info" }); } catch (err) { - ui.showToast(err.message, { type: "error" }); + showToast(err.message, { type: "error" }); } }, - [t, shares, share, ui] + [t, shares, share, showToast] ); const handleCopy = React.useCallback(() => { - ui.showToast(t("Share link copied"), { type: "info" }); - }, [t, ui]); + showToast(t("Share link copied"), { type: "info" }); + }, [t, showToast]); return ( <> diff --git a/app/scenes/APITokenNew.js b/app/scenes/APITokenNew.js index e84282e5..aea0a7eb 100644 --- a/app/scenes/APITokenNew.js +++ b/app/scenes/APITokenNew.js @@ -6,6 +6,7 @@ import Flex from "components/Flex"; import HelpText from "components/HelpText"; import Input from "components/Input"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = {| onSubmit: () => void, @@ -14,7 +15,8 @@ type Props = {| function APITokenNew({ onSubmit }: Props) { const [name, setName] = React.useState(""); const [isSaving, setIsSaving] = React.useState(false); - const { apiKeys, ui } = useStores(); + const { apiKeys } = useStores(); + const { showToast } = useToasts(); const { t } = useTranslation(); const handleSubmit = React.useCallback(async () => { @@ -22,14 +24,14 @@ function APITokenNew({ onSubmit }: Props) { try { await apiKeys.create({ name }); - ui.showToast(t("API token created", { type: "success" })); + showToast(t("API token created", { type: "success" })); onSubmit(); } catch (err) { - ui.showToast(err.message, { type: "error" }); + showToast(err.message, { type: "error" }); } finally { setIsSaving(false); } - }, [t, ui, name, onSubmit, apiKeys]); + }, [t, showToast, name, onSubmit, apiKeys]); const handleNameChange = React.useCallback((event) => { setName(event.target.value); diff --git a/app/scenes/Collection.js b/app/scenes/Collection.js index 2021bb32..bd1e5df7 100644 --- a/app/scenes/Collection.js +++ b/app/scenes/Collection.js @@ -43,6 +43,7 @@ import useBoolean from "hooks/useBoolean"; import useCurrentTeam from "hooks/useCurrentTeam"; import useImportDocument from "hooks/useImportDocument"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; import CollectionMenu from "menus/CollectionMenu"; import { newDocumentUrl, collectionUrl } from "utils/routeHelpers"; @@ -52,6 +53,7 @@ function CollectionScene() { const match = useRouteMatch(); const { t } = useTranslation(); const { documents, policies, collections, ui } = useStores(); + const { showToast } = useToasts(); const team = useCurrentTeam(); const [isFetching, setFetching] = React.useState(); const [error, setError] = React.useState(); @@ -108,11 +110,11 @@ function CollectionScene() { }, [collections, isFetching, collection, error, id, can]); const handleRejection = React.useCallback(() => { - ui.showToast( + showToast( t("Document not supported – try Markdown, Plain text, HTML, or Word"), { type: "error" } ); - }, [t, ui]); + }, [t, showToast]); if (!collection && error) { return ; diff --git a/app/scenes/CollectionDelete.js b/app/scenes/CollectionDelete.js index f9c23f34..d7e51170 100644 --- a/app/scenes/CollectionDelete.js +++ b/app/scenes/CollectionDelete.js @@ -7,7 +7,7 @@ import Collection from "models/Collection"; import Button from "components/Button"; import Flex from "components/Flex"; import HelpText from "components/HelpText"; -import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; import { homeUrl } from "utils/routeHelpers"; type Props = { @@ -17,7 +17,7 @@ type Props = { function CollectionDelete({ collection, onSubmit }: Props) { const [isDeleting, setIsDeleting] = React.useState(); - const { ui } = useStores(); + const { showToast } = useToasts(); const history = useHistory(); const { t } = useTranslation(); @@ -31,12 +31,12 @@ function CollectionDelete({ collection, onSubmit }: Props) { history.push(homeUrl()); onSubmit(); } catch (err) { - ui.showToast(err.message, { type: "error" }); + showToast(err.message, { type: "error" }); } finally { setIsDeleting(false); } }, - [ui, onSubmit, collection, history] + [showToast, onSubmit, collection, history] ); return ( diff --git a/app/scenes/CollectionEdit.js b/app/scenes/CollectionEdit.js index 0d4e3a2c..e08cf46a 100644 --- a/app/scenes/CollectionEdit.js +++ b/app/scenes/CollectionEdit.js @@ -4,7 +4,7 @@ import { inject, observer } from "mobx-react"; import * as React from "react"; import { withTranslation, Trans, type TFunction } from "react-i18next"; import AuthStore from "stores/AuthStore"; -import UiStore from "stores/UiStore"; +import ToastsStore from "stores/ToastsStore"; import Collection from "models/Collection"; import Button from "components/Button"; import Flex from "components/Flex"; @@ -16,7 +16,7 @@ import Switch from "components/Switch"; type Props = { collection: Collection, - ui: UiStore, + toasts: ToastsStore, auth: AuthStore, onSubmit: () => void, t: TFunction, @@ -46,11 +46,11 @@ class CollectionEdit extends React.Component { sort: this.sort, }); this.props.onSubmit(); - this.props.ui.showToast(t("The collection was updated"), { + this.props.toasts.showToast(t("The collection was updated"), { type: "success", }); } catch (err) { - this.props.ui.showToast(err.message, { type: "error" }); + this.props.toasts.showToast(err.message, { type: "error" }); } finally { this.isSaving = false; } @@ -148,5 +148,5 @@ class CollectionEdit extends React.Component { } export default withTranslation()( - inject("ui", "auth")(CollectionEdit) + inject("toasts", "auth")(CollectionEdit) ); diff --git a/app/scenes/CollectionNew.js b/app/scenes/CollectionNew.js index ae058bd8..91624ab8 100644 --- a/app/scenes/CollectionNew.js +++ b/app/scenes/CollectionNew.js @@ -7,7 +7,7 @@ import { withTranslation, type TFunction, Trans } from "react-i18next"; import { withRouter, type RouterHistory } from "react-router-dom"; import AuthStore from "stores/AuthStore"; import CollectionsStore from "stores/CollectionsStore"; -import UiStore from "stores/UiStore"; +import ToastsStore from "stores/ToastsStore"; import Collection from "models/Collection"; import Button from "components/Button"; import Flex from "components/Flex"; @@ -20,7 +20,7 @@ import Switch from "components/Switch"; type Props = { history: RouterHistory, auth: AuthStore, - ui: UiStore, + toasts: ToastsStore, collections: CollectionsStore, onSubmit: () => void, t: TFunction, @@ -55,7 +55,7 @@ class CollectionNew extends React.Component { this.props.onSubmit(); this.props.history.push(collection.url); } catch (err) { - this.props.ui.showToast(err.message, { type: "error" }); + this.props.toasts.showToast(err.message, { type: "error" }); } finally { this.isSaving = false; } @@ -169,5 +169,5 @@ class CollectionNew extends React.Component { } export default withTranslation()( - inject("collections", "ui", "auth")(withRouter(CollectionNew)) + inject("collections", "toasts", "auth")(withRouter(CollectionNew)) ); diff --git a/app/scenes/CollectionPermissions/AddGroupsToCollection.js b/app/scenes/CollectionPermissions/AddGroupsToCollection.js index 0ed8056b..86ac4912 100644 --- a/app/scenes/CollectionPermissions/AddGroupsToCollection.js +++ b/app/scenes/CollectionPermissions/AddGroupsToCollection.js @@ -8,7 +8,7 @@ import styled from "styled-components"; import AuthStore from "stores/AuthStore"; import CollectionGroupMembershipsStore from "stores/CollectionGroupMembershipsStore"; import GroupsStore from "stores/GroupsStore"; -import UiStore from "stores/UiStore"; +import ToastsStore from "stores/ToastsStore"; import Collection from "models/Collection"; import Group from "models/Group"; import GroupNew from "scenes/GroupNew"; @@ -23,7 +23,7 @@ import Modal from "components/Modal"; import PaginatedList from "components/PaginatedList"; type Props = { - ui: UiStore, + toasts: ToastsStore, auth: AuthStore, collection: Collection, collectionGroupMemberships: CollectionGroupMembershipsStore, @@ -65,14 +65,14 @@ class AddGroupsToCollection extends React.Component { groupId: group.id, permission: "read_write", }); - this.props.ui.showToast( + this.props.toasts.showToast( t("{{ groupName }} was added to the collection", { groupName: group.name, }), { type: "success" } ); } catch (err) { - this.props.ui.showToast(t("Could not add user"), { type: "error" }); + this.props.toasts.showToast(t("Could not add user"), { type: "error" }); console.error(err); } }; @@ -147,6 +147,6 @@ export default withTranslation()( "auth", "groups", "collectionGroupMemberships", - "ui" + "toasts" )(AddGroupsToCollection) ); diff --git a/app/scenes/CollectionPermissions/AddPeopleToCollection.js b/app/scenes/CollectionPermissions/AddPeopleToCollection.js index d92d93b7..6be89288 100644 --- a/app/scenes/CollectionPermissions/AddPeopleToCollection.js +++ b/app/scenes/CollectionPermissions/AddPeopleToCollection.js @@ -6,7 +6,7 @@ import * as React from "react"; import { withTranslation, type TFunction } from "react-i18next"; import AuthStore from "stores/AuthStore"; import MembershipsStore from "stores/MembershipsStore"; -import UiStore from "stores/UiStore"; +import ToastsStore from "stores/ToastsStore"; import UsersStore from "stores/UsersStore"; import Collection from "models/Collection"; import User from "models/User"; @@ -21,7 +21,7 @@ import PaginatedList from "components/PaginatedList"; import MemberListItem from "./components/MemberListItem"; type Props = { - ui: UiStore, + toasts: ToastsStore, auth: AuthStore, collection: Collection, memberships: MembershipsStore, @@ -62,14 +62,14 @@ class AddPeopleToCollection extends React.Component { userId: user.id, permission: "read_write", }); - this.props.ui.showToast( + this.props.toasts.showToast( t("{{ userName }} was added to the collection", { userName: user.name, }), { type: "success" } ); } catch (err) { - this.props.ui.showToast(t("Could not add user"), { type: "error" }); + this.props.toasts.showToast(t("Could not add user"), { type: "error" }); } }; @@ -130,5 +130,5 @@ class AddPeopleToCollection extends React.Component { } export default withTranslation()( - inject("auth", "users", "memberships", "ui")(AddPeopleToCollection) + inject("auth", "users", "memberships", "toasts")(AddPeopleToCollection) ); diff --git a/app/scenes/CollectionPermissions/index.js b/app/scenes/CollectionPermissions/index.js index d40bf163..513b3d60 100644 --- a/app/scenes/CollectionPermissions/index.js +++ b/app/scenes/CollectionPermissions/index.js @@ -20,6 +20,7 @@ import MemberListItem from "./components/MemberListItem"; import useBoolean from "hooks/useBoolean"; import useCurrentUser from "hooks/useCurrentUser"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = {| collection: Collection, @@ -29,12 +30,12 @@ function CollectionPermissions({ collection }: Props) { const { t } = useTranslation(); const user = useCurrentUser(); const { - ui, memberships, collectionGroupMemberships, users, groups, } = useStores(); + const { showToast } = useToasts(); const [ addGroupModalOpen, handleAddGroupModalOpen, @@ -53,7 +54,7 @@ function CollectionPermissions({ collection }: Props) { collectionId: collection.id, userId: user.id, }); - ui.showToast( + showToast( t(`{{ userName }} was removed from the collection`, { userName: user.name, }), @@ -62,10 +63,10 @@ function CollectionPermissions({ collection }: Props) { } ); } catch (err) { - ui.showToast(t("Could not remove user"), { type: "error" }); + showToast(t("Could not remove user"), { type: "error" }); } }, - [memberships, ui, collection, t] + [memberships, showToast, collection, t] ); const handleUpdateUser = React.useCallback( @@ -76,17 +77,17 @@ function CollectionPermissions({ collection }: Props) { userId: user.id, permission, }); - ui.showToast( + showToast( t(`{{ userName }} permissions were updated`, { userName: user.name }), { type: "success", } ); } catch (err) { - ui.showToast(t("Could not update user"), { type: "error" }); + showToast(t("Could not update user"), { type: "error" }); } }, - [memberships, ui, collection, t] + [memberships, showToast, collection, t] ); const handleRemoveGroup = React.useCallback( @@ -96,7 +97,7 @@ function CollectionPermissions({ collection }: Props) { collectionId: collection.id, groupId: group.id, }); - ui.showToast( + showToast( t(`The {{ groupName }} group was removed from the collection`, { groupName: group.name, }), @@ -105,10 +106,10 @@ function CollectionPermissions({ collection }: Props) { } ); } catch (err) { - ui.showToast(t("Could not remove group"), { type: "error" }); + showToast(t("Could not remove group"), { type: "error" }); } }, - [collectionGroupMemberships, ui, collection, t] + [collectionGroupMemberships, showToast, collection, t] ); const handleUpdateGroup = React.useCallback( @@ -119,7 +120,7 @@ function CollectionPermissions({ collection }: Props) { groupId: group.id, permission, }); - ui.showToast( + showToast( t(`{{ groupName }} permissions were updated`, { groupName: group.name, }), @@ -128,24 +129,24 @@ function CollectionPermissions({ collection }: Props) { } ); } catch (err) { - ui.showToast(t("Could not update user"), { type: "error" }); + showToast(t("Could not update user"), { type: "error" }); } }, - [collectionGroupMemberships, ui, collection, t] + [collectionGroupMemberships, showToast, collection, t] ); const handleChangePermission = React.useCallback( async (ev) => { try { await collection.save({ permission: ev.target.value }); - ui.showToast(t("Default access permissions were updated"), { + showToast(t("Default access permissions were updated"), { type: "success", }); } catch (err) { - ui.showToast(t("Could not update permissions"), { type: "error" }); + showToast(t("Could not update permissions"), { type: "error" }); } }, - [collection, ui, t] + [collection, showToast, t] ); const fetchOptions = React.useMemo(() => ({ id: collection.id }), [ diff --git a/app/scenes/Document/components/Document.js b/app/scenes/Document/components/Document.js index 25c173a4..7b326509 100644 --- a/app/scenes/Document/components/Document.js +++ b/app/scenes/Document/components/Document.js @@ -11,6 +11,7 @@ import type { RouterHistory, Match } from "react-router-dom"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; import AuthStore from "stores/AuthStore"; +import ToastsStore from "stores/ToastsStore"; import UiStore from "stores/UiStore"; import Document from "models/Document"; import Revision from "models/Revision"; @@ -59,6 +60,7 @@ type Props = { theme: Theme, auth: AuthStore, ui: UiStore, + toasts: ToastsStore, t: TFunction, }; @@ -89,8 +91,10 @@ class DocumentScene extends React.Component { } } else if (prevProps.document.revision !== this.lastRevision) { if (auth.user && document.updatedBy.id !== auth.user.id) { - this.props.ui.showToast( - t(`Document updated by ${document.updatedBy.name}`), + this.props.toasts.showToast( + t(`Document updated by {{userName}}`, { + userName: document.updatedBy.name, + }), { timeout: 30 * 1000, type: "warning", @@ -230,7 +234,7 @@ class DocumentScene extends React.Component { this.props.ui.setActiveDocument(savedDocument); } } catch (err) { - this.props.ui.showToast(err.message, { type: "error" }); + this.props.toasts.showToast(err.message, { type: "error" }); } finally { this.isSaving = false; this.isPublishing = false; @@ -525,6 +529,6 @@ const MaxWidth = styled(Flex)` export default withRouter( withTranslation()( - inject("ui", "auth", "policies", "revisions")(DocumentScene) + inject("ui", "auth", "policies", "revisions", "toasts")(DocumentScene) ) ); diff --git a/app/scenes/Document/components/SharePopover.js b/app/scenes/Document/components/SharePopover.js index f0500522..ca3160d5 100644 --- a/app/scenes/Document/components/SharePopover.js +++ b/app/scenes/Document/components/SharePopover.js @@ -16,6 +16,7 @@ import Input from "components/Input"; import Notice from "components/Notice"; import Switch from "components/Switch"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = {| document: Document, @@ -26,7 +27,8 @@ type Props = {| function SharePopover({ document, share, sharedParent, onSubmit }: Props) { const { t } = useTranslation(); - const { policies, shares, ui } = useStores(); + const { policies, shares } = useStores(); + const { showToast } = useToasts(); const [isCopied, setIsCopied] = React.useState(false); const timeout = React.useRef(); const can = policies.abilities(share ? share.id : ""); @@ -46,10 +48,10 @@ function SharePopover({ document, share, sharedParent, onSubmit }: Props) { try { await share.save({ published: event.currentTarget.checked }); } catch (err) { - ui.showToast(err.message, { type: "error" }); + showToast(err.message, { type: "error" }); } }, - [document.id, shares, ui] + [document.id, shares, showToast] ); const handleChildDocumentsChange = React.useCallback( @@ -62,10 +64,10 @@ function SharePopover({ document, share, sharedParent, onSubmit }: Props) { includeChildDocuments: event.currentTarget.checked, }); } catch (err) { - ui.showToast(err.message, { type: "error" }); + showToast(err.message, { type: "error" }); } }, - [document.id, shares, ui] + [document.id, shares, showToast] ); const handleCopied = React.useCallback(() => { @@ -75,9 +77,9 @@ function SharePopover({ document, share, sharedParent, onSubmit }: Props) { setIsCopied(false); onSubmit(); - ui.showToast(t("Share link copied"), { type: "info" }); + showToast(t("Share link copied"), { type: "info" }); }, 250); - }, [t, onSubmit, ui]); + }, [t, onSubmit, showToast]); return ( <> diff --git a/app/scenes/DocumentDelete.js b/app/scenes/DocumentDelete.js index 1b1ba032..ff39b0dc 100644 --- a/app/scenes/DocumentDelete.js +++ b/app/scenes/DocumentDelete.js @@ -8,6 +8,7 @@ import Button from "components/Button"; import Flex from "components/Flex"; import HelpText from "components/HelpText"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; import { collectionUrl, documentUrl } from "utils/routeHelpers"; type Props = { @@ -21,7 +22,7 @@ function DocumentDelete({ document, onSubmit }: Props) { const history = useHistory(); const [isDeleting, setDeleting] = React.useState(false); const [isArchiving, setArchiving] = React.useState(false); - const { showToast } = ui; + const { showToast } = useToasts(); const canArchive = !document.isDraft && !document.isArchived; const collection = collections.get(document.collectionId); diff --git a/app/scenes/DocumentMove.js b/app/scenes/DocumentMove.js index 08b1ae90..94a1048a 100644 --- a/app/scenes/DocumentMove.js +++ b/app/scenes/DocumentMove.js @@ -15,6 +15,7 @@ import { Outline } from "components/Input"; import Labeled from "components/Labeled"; import PathToDocument from "components/PathToDocument"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = {| document: Document, @@ -23,7 +24,8 @@ type Props = {| function DocumentMove({ document, onRequestClose }: Props) { const [searchTerm, setSearchTerm] = useState(); - const { ui, collections, documents } = useStores(); + const { collections, documents } = useStores(); + const { showToast } = useToasts(); const { t } = useTranslation(); const searchIndex = useMemo(() => { @@ -77,7 +79,7 @@ function DocumentMove({ document, onRequestClose }: Props) { }, [document, collections, searchTerm, searchIndex]); const handleSuccess = () => { - ui.showToast(t("Document moved"), { type: "info" }); + showToast(t("Document moved"), { type: "info" }); onRequestClose(); }; diff --git a/app/scenes/DocumentNew.js b/app/scenes/DocumentNew.js index 14a57447..474e72c9 100644 --- a/app/scenes/DocumentNew.js +++ b/app/scenes/DocumentNew.js @@ -9,6 +9,7 @@ import CenteredContent from "components/CenteredContent"; import Flex from "components/Flex"; import PlaceholderDocument from "components/PlaceholderDocument"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; import { editDocumentUrl } from "utils/routeHelpers"; function DocumentNew() { @@ -16,7 +17,8 @@ function DocumentNew() { const location = useLocation(); const match = useRouteMatch(); const { t } = useTranslation(); - const { documents, ui, collections } = useStores(); + const { documents, collections } = useStores(); + const { showToast } = useToasts(); const id = match.params.id || ""; useEffect(() => { @@ -36,7 +38,7 @@ function DocumentNew() { history.replace(editDocumentUrl(document)); } catch (err) { - ui.showToast(t("Couldn’t create the document, try again?"), { + showToast(t("Couldn’t create the document, try again?"), { type: "error", }); history.goBack(); diff --git a/app/scenes/DocumentPermanentDelete.js b/app/scenes/DocumentPermanentDelete.js index 47944f6b..bf6d2824 100644 --- a/app/scenes/DocumentPermanentDelete.js +++ b/app/scenes/DocumentPermanentDelete.js @@ -8,6 +8,7 @@ import Button from "components/Button"; import Flex from "components/Flex"; import HelpText from "components/HelpText"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = {| document: Document, @@ -17,8 +18,8 @@ type Props = {| function DocumentPermanentDelete({ document, onSubmit }: Props) { const [isDeleting, setIsDeleting] = React.useState(false); const { t } = useTranslation(); - const { ui, documents } = useStores(); - const { showToast } = ui; + const { documents } = useStores(); + const { showToast } = useToasts(); const history = useHistory(); const handleSubmit = React.useCallback( diff --git a/app/scenes/DocumentTemplatize.js b/app/scenes/DocumentTemplatize.js index 814220fb..b4cec0d7 100644 --- a/app/scenes/DocumentTemplatize.js +++ b/app/scenes/DocumentTemplatize.js @@ -8,7 +8,7 @@ import Document from "models/Document"; import Button from "components/Button"; import Flex from "components/Flex"; import HelpText from "components/HelpText"; -import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; import { documentUrl } from "utils/routeHelpers"; type Props = { @@ -19,7 +19,7 @@ type Props = { function DocumentTemplatize({ document, onSubmit }: Props) { const [isSaving, setIsSaving] = useState(); const history = useHistory(); - const { ui } = useStores(); + const { showToast } = useToasts(); const { t } = useTranslation(); const handleSubmit = React.useCallback( @@ -30,17 +30,17 @@ function DocumentTemplatize({ document, onSubmit }: Props) { try { const template = await document.templatize(); history.push(documentUrl(template)); - ui.showToast(t("Template created, go ahead and customize it"), { + showToast(t("Template created, go ahead and customize it"), { type: "info", }); onSubmit(); } catch (err) { - ui.showToast(err.message, { type: "error" }); + showToast(err.message, { type: "error" }); } finally { setIsSaving(false); } }, - [document, ui, history, onSubmit, t] + [document, showToast, history, onSubmit, t] ); return ( diff --git a/app/scenes/GroupDelete.js b/app/scenes/GroupDelete.js index 0f1581c7..035269ca 100644 --- a/app/scenes/GroupDelete.js +++ b/app/scenes/GroupDelete.js @@ -8,7 +8,7 @@ import Group from "models/Group"; import Button from "components/Button"; import Flex from "components/Flex"; import HelpText from "components/HelpText"; -import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = {| group: Group, @@ -16,8 +16,8 @@ type Props = {| |}; function GroupDelete({ group, onSubmit }: Props) { - const { ui } = useStores(); const { t } = useTranslation(); + const { showToast } = useToasts(); const history = useHistory(); const [isDeleting, setIsDeleting] = React.useState(); @@ -30,7 +30,7 @@ function GroupDelete({ group, onSubmit }: Props) { history.push(groupSettings()); onSubmit(); } catch (err) { - ui.showToast(err.message, { type: "error" }); + showToast(err.message, { type: "error" }); } finally { setIsDeleting(false); } diff --git a/app/scenes/GroupEdit.js b/app/scenes/GroupEdit.js index 7f24ecc4..89dc4a1c 100644 --- a/app/scenes/GroupEdit.js +++ b/app/scenes/GroupEdit.js @@ -7,7 +7,7 @@ import Button from "components/Button"; import Flex from "components/Flex"; import HelpText from "components/HelpText"; import Input from "components/Input"; -import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = { group: Group, @@ -15,7 +15,7 @@ type Props = { }; function GroupEdit({ group, onSubmit }: Props) { - const { ui } = useStores(); + const { showToast } = useToasts(); const { t } = useTranslation(); const [name, setName] = React.useState(group.name); const [isSaving, setIsSaving] = React.useState(); @@ -29,12 +29,12 @@ function GroupEdit({ group, onSubmit }: Props) { await group.save({ name: name }); onSubmit(); } catch (err) { - ui.showToast(err.message, { type: "error" }); + showToast(err.message, { type: "error" }); } finally { setIsSaving(false); } }, - [group, onSubmit, ui, name] + [group, onSubmit, showToast, name] ); const handleNameChange = React.useCallback((ev: SyntheticInputEvent<*>) => { diff --git a/app/scenes/GroupMembers/AddPeopleToGroup.js b/app/scenes/GroupMembers/AddPeopleToGroup.js index 6156d4d2..9165b2ac 100644 --- a/app/scenes/GroupMembers/AddPeopleToGroup.js +++ b/app/scenes/GroupMembers/AddPeopleToGroup.js @@ -6,7 +6,7 @@ import * as React from "react"; import { withTranslation, type TFunction } from "react-i18next"; import AuthStore from "stores/AuthStore"; import GroupMembershipsStore from "stores/GroupMembershipsStore"; -import UiStore from "stores/UiStore"; +import ToastsStore from "stores/ToastsStore"; import UsersStore from "stores/UsersStore"; import Group from "models/Group"; import User from "models/User"; @@ -21,7 +21,7 @@ import PaginatedList from "components/PaginatedList"; import GroupMemberListItem from "./components/GroupMemberListItem"; type Props = { - ui: UiStore, + toasts: ToastsStore, auth: AuthStore, group: Group, groupMemberships: GroupMembershipsStore, @@ -62,12 +62,12 @@ class AddPeopleToGroup extends React.Component { groupId: this.props.group.id, userId: user.id, }); - this.props.ui.showToast( + this.props.toasts.showToast( t(`{{userName}} was added to the group`, { userName: user.name }), { type: "success" } ); } catch (err) { - this.props.ui.showToast(t("Could not add user"), { type: "error" }); + this.props.toasts.showToast(t("Could not add user"), { type: "error" }); } }; @@ -128,5 +128,5 @@ class AddPeopleToGroup extends React.Component { } export default withTranslation()( - inject("auth", "users", "groupMemberships", "ui")(AddPeopleToGroup) + inject("auth", "users", "groupMemberships", "toasts")(AddPeopleToGroup) ); diff --git a/app/scenes/GroupMembers/GroupMembers.js b/app/scenes/GroupMembers/GroupMembers.js index 8dd378b1..27371bd1 100644 --- a/app/scenes/GroupMembers/GroupMembers.js +++ b/app/scenes/GroupMembers/GroupMembers.js @@ -15,6 +15,7 @@ import Subheading from "components/Subheading"; import AddPeopleToGroup from "./AddPeopleToGroup"; import GroupMemberListItem from "./components/GroupMemberListItem"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = { group: Group, @@ -22,7 +23,8 @@ type Props = { function GroupMembers({ group }: Props) { const [addModalOpen, setAddModalOpen] = React.useState(); - const { users, groupMemberships, policies, ui } = useStores(); + const { users, groupMemberships, policies } = useStores(); + const { showToast } = useToasts(); const { t } = useTranslation(); const can = policies.abilities(group.id); @@ -36,12 +38,12 @@ function GroupMembers({ group }: Props) { groupId: group.id, userId: user.id, }); - ui.showToast( + showToast( t(`{{userName}} was removed from the group`, { userName: user.name }), { type: "success" } ); } catch (err) { - ui.showToast(t("Could not remove user"), { type: "error" }); + showToast(t("Could not remove user"), { type: "error" }); } }; diff --git a/app/scenes/GroupNew.js b/app/scenes/GroupNew.js index 6ed74813..b3ecdd52 100644 --- a/app/scenes/GroupNew.js +++ b/app/scenes/GroupNew.js @@ -10,14 +10,16 @@ import HelpText from "components/HelpText"; import Input from "components/Input"; import Modal from "components/Modal"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = { onSubmit: () => void, }; function GroupNew({ onSubmit }: Props) { - const { ui, groups } = useStores(); + const { groups } = useStores(); const { t } = useTranslation(); + const { showToast } = useToasts(); const [name, setName] = React.useState(); const [isSaving, setIsSaving] = React.useState(); const [group, setGroup] = React.useState(); @@ -35,7 +37,7 @@ function GroupNew({ onSubmit }: Props) { try { setGroup(await group.save()); } catch (err) { - ui.showToast(err.message, { type: "error" }); + showToast(err.message, { type: "error" }); } finally { setIsSaving(false); } diff --git a/app/scenes/Invite.js b/app/scenes/Invite.js index 94291ecd..3e899a67 100644 --- a/app/scenes/Invite.js +++ b/app/scenes/Invite.js @@ -15,6 +15,7 @@ import Tooltip from "components/Tooltip"; import useCurrentTeam from "hooks/useCurrentTeam"; import useCurrentUser from "hooks/useCurrentUser"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; const MAX_INVITES = 20; @@ -36,7 +37,8 @@ function Invite({ onSubmit }: Props) { { email: "", name: "" }, ]); - const { users, policies, ui } = useStores(); + const { users, policies } = useStores(); + const { showToast } = useToasts(); const user = useCurrentUser(); const team = useCurrentTeam(); const { t } = useTranslation(); @@ -52,14 +54,14 @@ function Invite({ onSubmit }: Props) { try { await users.invite(invites); onSubmit(); - ui.showToast(t("We sent out your invites!"), { type: "success" }); + showToast(t("We sent out your invites!"), { type: "success" }); } catch (err) { - ui.showToast(err.message, { type: "error" }); + showToast(err.message, { type: "error" }); } finally { setIsSaving(false); } }, - [onSubmit, ui, invites, t, users] + [onSubmit, showToast, invites, t, users] ); const handleChange = React.useCallback((ev, index) => { @@ -72,7 +74,7 @@ function Invite({ onSubmit }: Props) { const handleAdd = React.useCallback(() => { if (invites.length >= MAX_INVITES) { - ui.showToast( + showToast( t("Sorry, you can only send {{MAX_INVITES}} invites at a time", { MAX_INVITES, }), @@ -85,7 +87,7 @@ function Invite({ onSubmit }: Props) { newInvites.push({ email: "", name: "" }); return newInvites; }); - }, [ui, invites, t]); + }, [showToast, invites, t]); const handleRemove = React.useCallback( (ev: SyntheticEvent<>, index: number) => { @@ -102,10 +104,10 @@ function Invite({ onSubmit }: Props) { const handleCopy = React.useCallback(() => { setLinkCopied(true); - ui.showToast(t("Share link copied"), { + showToast(t("Share link copied"), { type: "success", }); - }, [ui, t]); + }, [showToast, t]); return (
diff --git a/app/scenes/Settings/Details.js b/app/scenes/Settings/Details.js index 8b5de8a4..6d938625 100644 --- a/app/scenes/Settings/Details.js +++ b/app/scenes/Settings/Details.js @@ -15,9 +15,11 @@ import ImageUpload from "./components/ImageUpload"; import env from "env"; import useCurrentTeam from "hooks/useCurrentTeam"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; function Details() { - const { auth, ui } = useStores(); + const { auth } = useStores(); + const { showToast } = useToasts(); const team = useCurrentTeam(); const { t } = useTranslation(); const form = useRef(); @@ -37,12 +39,12 @@ function Details() { avatarUrl, subdomain, }); - ui.showToast(t("Settings saved"), { type: "success" }); + showToast(t("Settings saved"), { type: "success" }); } catch (err) { - ui.showToast(err.message, { type: "error" }); + showToast(err.message, { type: "error" }); } }, - [auth, ui, name, avatarUrl, subdomain, t] + [auth, showToast, name, avatarUrl, subdomain, t] ); const handleNameChange = React.useCallback((ev: SyntheticInputEvent<*>) => { @@ -66,9 +68,9 @@ function Details() { const handleAvatarError = React.useCallback( (error: ?string) => { - ui.showToast(error || t("Unable to upload new logo")); + showToast(error || t("Unable to upload new logo")); }, - [ui, t] + [showToast, t] ); const isValid = form.current && form.current.checkValidity(); diff --git a/app/scenes/Settings/ImportExport.js b/app/scenes/Settings/ImportExport.js index f8188dde..f56ceba5 100644 --- a/app/scenes/Settings/ImportExport.js +++ b/app/scenes/Settings/ImportExport.js @@ -14,6 +14,7 @@ import Notice from "components/Notice"; import Scene from "components/Scene"; 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"; @@ -21,8 +22,8 @@ function ImportExport() { const { t } = useTranslation(); const user = useCurrentUser(); const fileRef = React.useRef(); - const { ui, collections } = useStores(); - const { showToast } = ui; + const { collections } = useStores(); + const { showToast } = useToasts(); const [isLoading, setLoading] = React.useState(false); const [isImporting, setImporting] = React.useState(false); const [isImported, setImported] = React.useState(false); diff --git a/app/scenes/Settings/Notifications.js b/app/scenes/Settings/Notifications.js index 52729f6f..edcc0013 100644 --- a/app/scenes/Settings/Notifications.js +++ b/app/scenes/Settings/Notifications.js @@ -14,9 +14,11 @@ import Subheading from "components/Subheading"; import NotificationListItem from "./components/NotificationListItem"; import useCurrentUser from "hooks/useCurrentUser"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; function Notifications() { - const { notificationSettings, ui } = useStores(); + const { notificationSettings } = useStores(); + const { showToast } = useToasts(); const user = useCurrentUser(); const { t } = useTranslation(); @@ -64,7 +66,7 @@ function Notifications() { }, [notificationSettings]); const showSuccessMessage = debounce(() => { - ui.showToast(t("Notifications saved"), { type: "success" }); + showToast(t("Notifications saved"), { type: "success" }); }, 500); const handleChange = React.useCallback( diff --git a/app/scenes/Settings/Profile.js b/app/scenes/Settings/Profile.js index 4efedaae..725ac764 100644 --- a/app/scenes/Settings/Profile.js +++ b/app/scenes/Settings/Profile.js @@ -7,7 +7,7 @@ import { Trans, withTranslation, type TFunction } from "react-i18next"; import styled from "styled-components"; import { languageOptions } from "shared/i18n"; import AuthStore from "stores/AuthStore"; -import UiStore from "stores/UiStore"; +import ToastsStore from "stores/ToastsStore"; import UserDelete from "scenes/UserDelete"; import Button from "components/Button"; import Flex from "components/Flex"; @@ -20,7 +20,7 @@ import ImageUpload from "./components/ImageUpload"; type Props = { auth: AuthStore, - ui: UiStore, + toasts: ToastsStore, t: TFunction, }; @@ -55,7 +55,7 @@ class Profile extends React.Component { language: this.language, }); - this.props.ui.showToast(t("Profile saved"), { type: "success" }); + this.props.toasts.showToast(t("Profile saved"), { type: "success" }); }; handleNameChange = (ev: SyntheticInputEvent<*>) => { @@ -69,12 +69,14 @@ class Profile extends React.Component { await this.props.auth.updateUser({ avatarUrl: this.avatarUrl, }); - this.props.ui.showToast(t("Profile picture updated"), { type: "success" }); + this.props.toasts.showToast(t("Profile picture updated"), { + type: "success", + }); }; handleAvatarError = (error: ?string) => { const { t } = this.props; - this.props.ui.showToast( + this.props.toasts.showToast( error || t("Unable to upload new profile picture"), { type: "error" } ); @@ -213,4 +215,4 @@ const Avatar = styled.img` ${avatarStyles}; `; -export default withTranslation()(inject("auth", "ui")(Profile)); +export default withTranslation()(inject("auth", "toasts")(Profile)); diff --git a/app/scenes/Settings/Security.js b/app/scenes/Settings/Security.js index 9b99454a..3a065fe2 100644 --- a/app/scenes/Settings/Security.js +++ b/app/scenes/Settings/Security.js @@ -11,17 +11,19 @@ import HelpText from "components/HelpText"; import Scene from "components/Scene"; import useCurrentTeam from "hooks/useCurrentTeam"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; function Security() { - const { auth, ui } = useStores(); + const { auth } = useStores(); const team = useCurrentTeam(); const { t } = useTranslation(); + const { showToast } = useToasts(); const [sharing, setSharing] = useState(team.documentEmbeds); const [documentEmbeds, setDocumentEmbeds] = useState(team.guestSignin); const [guestSignin, setGuestSignin] = useState(team.sharing); const showSuccessMessage = debounce(() => { - ui.showToast(t("Settings saved"), { type: "success" }); + showToast(t("Settings saved"), { type: "success" }); }, 500); const handleChange = React.useCallback( diff --git a/app/scenes/UserDelete.js b/app/scenes/UserDelete.js index 571de217..3300aa93 100644 --- a/app/scenes/UserDelete.js +++ b/app/scenes/UserDelete.js @@ -7,6 +7,7 @@ import Flex from "components/Flex"; import HelpText from "components/HelpText"; import Modal from "components/Modal"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; type Props = {| onRequestClose: () => void, @@ -14,7 +15,8 @@ type Props = {| function UserDelete({ onRequestClose }: Props) { const [isDeleting, setIsDeleting] = React.useState(); - const { auth, ui } = useStores(); + const { auth } = useStores(); + const { showToast } = useToasts(); const { t } = useTranslation(); const handleSubmit = React.useCallback( @@ -26,12 +28,12 @@ function UserDelete({ onRequestClose }: Props) { await auth.deleteUser(); auth.logout(); } catch (error) { - ui.showToast(error.message, { type: "error" }); + showToast(error.message, { type: "error" }); } finally { setIsDeleting(false); } }, - [auth, ui] + [auth, showToast] ); return ( diff --git a/app/stores/RootStore.js b/app/stores/RootStore.js index 11a4e06b..d5682179 100644 --- a/app/stores/RootStore.js +++ b/app/stores/RootStore.js @@ -13,6 +13,7 @@ import NotificationSettingsStore from "./NotificationSettingsStore"; import PoliciesStore from "./PoliciesStore"; import RevisionsStore from "./RevisionsStore"; import SharesStore from "./SharesStore"; +import ToastsStore from "./ToastsStore"; import UiStore from "./UiStore"; import UsersStore from "./UsersStore"; import ViewsStore from "./ViewsStore"; @@ -35,6 +36,7 @@ export default class RootStore { ui: UiStore; users: UsersStore; views: ViewsStore; + toasts: ToastsStore; constructor() { this.apiKeys = new ApiKeysStore(this); @@ -54,6 +56,7 @@ export default class RootStore { this.ui = new UiStore(); this.users = new UsersStore(this); this.views = new ViewsStore(this); + this.toasts = new ToastsStore(); } logout() { diff --git a/app/stores/ToastsStore.js b/app/stores/ToastsStore.js new file mode 100644 index 00000000..65bbb656 --- /dev/null +++ b/app/stores/ToastsStore.js @@ -0,0 +1,52 @@ +// @flow +import { orderBy } from "lodash"; +import { observable, action, computed } from "mobx"; +import { v4 as uuidv4 } from "uuid"; +import type { Toast, ToastOptions } from "types"; + +export default class ToastsStore { + @observable toasts: Map = new Map(); + lastToastId: string; + + @action + showToast = ( + message: string, + options: ToastOptions = { + type: "info", + } + ) => { + if (!message) return; + + const lastToast = this.toasts.get(this.lastToastId); + if (lastToast && lastToast.message === message) { + this.toasts.set(this.lastToastId, { + ...lastToast, + reoccurring: lastToast.reoccurring ? ++lastToast.reoccurring : 1, + }); + return this.lastToastId; + } + + const id = uuidv4(); + const createdAt = new Date().toISOString(); + this.toasts.set(id, { + id, + message, + createdAt, + type: options.type, + timeout: options.timeout, + action: options.action, + }); + this.lastToastId = id; + return id; + }; + + @action + hideToast = (id: string) => { + this.toasts.delete(id); + }; + + @computed + get orderedData(): Toast[] { + return orderBy(Array.from(this.toasts.values()), "createdAt", "desc"); + } +} diff --git a/app/stores/UiStore.test.js b/app/stores/ToastsStore.test.js similarity index 53% rename from app/stores/UiStore.test.js rename to app/stores/ToastsStore.test.js index 4842cb25..f7bfb86d 100644 --- a/app/stores/UiStore.test.js +++ b/app/stores/ToastsStore.test.js @@ -2,27 +2,27 @@ import stores from '.'; // Actions -describe('UiStore', () => { +describe('ToastsStore', () => { let store; beforeEach(() => { - store = stores.ui; + store = stores.toasts; }); test('#add should add messages', () => { - expect(store.orderedToasts.length).toBe(0); + expect(store.orderedData.length).toBe(0); store.showToast('first error'); store.showToast('second error'); - expect(store.orderedToasts.length).toBe(2); + expect(store.orderedData.length).toBe(2); }); test('#remove should remove messages', () => { store.toasts.clear(); const id = store.showToast('first error'); store.showToast('second error'); - expect(store.orderedToasts.length).toBe(2); - store.removeToast(id); - expect(store.orderedToasts.length).toBe(1); - expect(store.orderedToasts[0].message).toBe('second error'); + expect(store.orderedData.length).toBe(2); + store.hideToast(id); + expect(store.orderedData.length).toBe(1); + expect(store.orderedData[0].message).toBe('second error'); }); }); diff --git a/app/stores/UiStore.js b/app/stores/UiStore.js index a4638e30..c4050eee 100644 --- a/app/stores/UiStore.js +++ b/app/stores/UiStore.js @@ -1,11 +1,8 @@ // @flow -import { orderBy } from "lodash"; -import { observable, action, autorun, computed } from "mobx"; -import { v4 as uuidv4 } from "uuid"; +import { action, autorun, computed, observable } from "mobx"; import { light as defaultTheme } from "shared/styles/theme"; import Collection from "models/Collection"; import Document from "models/Document"; -import type { Toast } from "types"; const UI_STORE = "UI_STORE"; @@ -27,8 +24,6 @@ class UiStore { @observable sidebarWidth: number; @observable sidebarCollapsed: boolean = false; @observable sidebarIsResizing: boolean = false; - @observable toasts: Map = new Map(); - lastToastId: string; constructor() { // Rehydrate @@ -173,50 +168,6 @@ class UiStore { this.mobileSidebarVisible = false; }; - @action - showToast = ( - message: string, - options: { - type: "warning" | "error" | "info" | "success", - timeout?: number, - action?: { - text: string, - onClick: () => void, - }, - } = { - type: "info", - } - ) => { - if (!message) return; - - const lastToast = this.toasts.get(this.lastToastId); - if (lastToast && lastToast.message === message) { - this.toasts.set(this.lastToastId, { - ...lastToast, - reoccurring: lastToast.reoccurring ? ++lastToast.reoccurring : 1, - }); - return; - } - - const id = uuidv4(); - const createdAt = new Date().toISOString(); - this.toasts.set(id, { - id, - message, - createdAt, - type: options.type, - timeout: options.timeout, - action: options.action, - }); - this.lastToastId = id; - return id; - }; - - @action - removeToast = (id: string) => { - this.toasts.delete(id); - }; - @computed get resolvedTheme(): "dark" | "light" { if (this.theme === "system") { @@ -226,11 +177,6 @@ class UiStore { return this.theme; } - @computed - get orderedToasts(): Toast[] { - return orderBy(Array.from(this.toasts.values()), "createdAt", "desc"); - } - @computed get asJson(): string { return JSON.stringify({ diff --git a/app/types/index.js b/app/types/index.js index b6f31049..0aecebc6 100644 --- a/app/types/index.js +++ b/app/types/index.js @@ -99,3 +99,12 @@ export type MenuItem = visible?: boolean, title: React.Node, |}; + +export type ToastOptions = {| + type: "warning" | "error" | "info" | "success", + timeout?: number, + action?: { + text: string, + onClick: () => void, + }, +|};