fix: Separate toasts storage to own MobX store (#2339)

Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
Saumya Pandey
2021-07-20 14:36:10 +05:30
committed by GitHub
parent f64ab37d3c
commit fdb85ec195
45 changed files with 297 additions and 231 deletions

View File

@ -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",
})

View File

@ -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(() => {

View File

@ -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",

View File

@ -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;

View File

@ -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<void>,
@ -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 (
<>

View File

@ -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<Props> {
const {
auth,
ui,
toasts,
documents,
collections,
groups,
@ -113,7 +113,7 @@ class SocketProvider extends React.Component<Props> {
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<Props> {
export default inject(
"auth",
"ui",
"toasts",
"documents",
"collections",
"groups",

View File

@ -6,15 +6,15 @@ import Toast from "components/Toast";
import useStores from "hooks/useStores";
function Toasts() {
const { ui } = useStores();
const { toasts } = useStores();
return (
<List>
{ui.orderedToasts.map((toast) => (
{toasts.orderedData.map((toast) => (
<Toast
key={toast.id}
toast={toast}
onRequestClose={() => ui.removeToast(toast.id)}
onRequestClose={() => toasts.hideToast(toast.id)}
/>
))}
</List>

View File

@ -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<void>, 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 {

8
app/hooks/useToasts.js Normal file
View File

@ -0,0 +1,8 @@
// @flow
import useStores from "./useStores";
export default function useToasts() {
const { toasts } = useStores();
return { showToast: toasts.showToast, hideToast: toasts.hideToast };
}

View File

@ -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);

View File

@ -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 (

View File

@ -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,

View File

@ -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 (
<>

View File

@ -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);

View File

@ -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 <Search notFound />;

View File

@ -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 (

View File

@ -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<Props> {
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<Props> {
}
export default withTranslation()<CollectionEdit>(
inject("ui", "auth")(CollectionEdit)
inject("toasts", "auth")(CollectionEdit)
);

View File

@ -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<Props> {
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<Props> {
}
export default withTranslation()<CollectionNew>(
inject("collections", "ui", "auth")(withRouter(CollectionNew))
inject("collections", "toasts", "auth")(withRouter(CollectionNew))
);

View File

@ -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<Props> {
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()<AddGroupsToCollection>(
"auth",
"groups",
"collectionGroupMemberships",
"ui"
"toasts"
)(AddGroupsToCollection)
);

View File

@ -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<Props> {
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<Props> {
}
export default withTranslation()<AddPeopleToCollection>(
inject("auth", "users", "memberships", "ui")(AddPeopleToCollection)
inject("auth", "users", "memberships", "toasts")(AddPeopleToCollection)
);

View File

@ -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 }), [

View File

@ -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<Props> {
}
} 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<Props> {
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()<DocumentScene>(
inject("ui", "auth", "policies", "revisions")(DocumentScene)
inject("ui", "auth", "policies", "revisions", "toasts")(DocumentScene)
)
);

View File

@ -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<?TimeoutID>();
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 (
<>

View File

@ -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);

View File

@ -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();
};

View File

@ -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("Couldnt create the document, try again?"), {
showToast(t("Couldnt create the document, try again?"), {
type: "error",
});
history.goBack();

View File

@ -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(

View File

@ -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 (

View File

@ -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);
}

View File

@ -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<*>) => {

View File

@ -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<Props> {
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<Props> {
}
export default withTranslation()<AddPeopleToGroup>(
inject("auth", "users", "groupMemberships", "ui")(AddPeopleToGroup)
inject("auth", "users", "groupMemberships", "toasts")(AddPeopleToGroup)
);

View File

@ -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" });
}
};

View File

@ -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);
}

View File

@ -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 (
<form onSubmit={handleSubmit}>

View File

@ -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<?HTMLFormElement>();
@ -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();

View File

@ -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);

View File

@ -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(

View File

@ -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<Props> {
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<Props> {
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()<Profile>(inject("auth", "ui")(Profile));
export default withTranslation()<Profile>(inject("auth", "toasts")(Profile));

View File

@ -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(

View File

@ -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 (

View File

@ -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() {

52
app/stores/ToastsStore.js Normal file
View File

@ -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<string, Toast> = 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");
}
}

View File

@ -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');
});
});

View File

@ -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<string, Toast> = 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({

View File

@ -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,
},
|};