Move toasts to sonner (#6053)

This commit is contained in:
Tom Moor
2023-10-22 17:30:24 -04:00
committed by GitHub
parent 389297a337
commit ef76405bd6
92 changed files with 363 additions and 1015 deletions

View File

@@ -5,6 +5,7 @@ import Initials from "./Initials";
export enum AvatarSize {
Small = 16,
Toast = 18,
Medium = 24,
Large = 32,
XLarge = 48,

View File

@@ -2,12 +2,12 @@ import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { useHistory } from "react-router-dom";
import { toast } from "sonner";
import Collection from "~/models/Collection";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Text from "~/components/Text";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import { homePath } from "~/utils/routeHelpers";
type Props = {
@@ -18,7 +18,6 @@ type Props = {
function CollectionDeleteDialog({ collection, onSubmit }: Props) {
const team = useCurrentTeam();
const { ui } = useStores();
const { showToast } = useToasts();
const history = useHistory();
const { t } = useTranslation();
@@ -31,7 +30,7 @@ function CollectionDeleteDialog({ collection, onSubmit }: Props) {
await collection.delete();
onSubmit();
showToast(t("Collection deleted"), { type: "success" });
toast.success(t("Collection deleted"));
};
return (

View File

@@ -3,6 +3,7 @@ import { observer } from "mobx-react";
import { transparentize } from "polished";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { s } from "@shared/styles";
import Collection from "~/models/Collection";
@@ -13,7 +14,6 @@ import LoadingIndicator from "~/components/LoadingIndicator";
import NudeButton from "~/components/NudeButton";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
type Props = {
collection: Collection;
@@ -21,7 +21,6 @@ type Props = {
function CollectionDescription({ collection }: Props) {
const { collections } = useStores();
const { showToast } = useToasts();
const { t } = useTranslation();
const [isExpanded, setExpanded] = React.useState(false);
const [isEditing, setEditing] = React.useState(false);
@@ -59,15 +58,11 @@ function CollectionDescription({ collection }: Props) {
});
setDirty(false);
} catch (err) {
showToast(
t("Sorry, an error occurred saving the collection", {
type: "error",
})
);
toast.error(t("Sorry, an error occurred saving the collection"));
throw err;
}
}, 1000),
[collection, showToast, t]
[collection, t]
);
const handleChange = React.useCallback(

View File

@@ -1,11 +1,11 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { toast } from "sonner";
import Comment from "~/models/Comment";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
type Props = {
comment: Comment;
@@ -14,7 +14,6 @@ type Props = {
function CommentDeleteDialog({ comment, onSubmit }: Props) {
const { comments } = useStores();
const { showToast } = useToasts();
const { t } = useTranslation();
const hasChildComments = comments.inThread(comment.id).length > 1;
@@ -23,7 +22,7 @@ function CommentDeleteDialog({ comment, onSubmit }: Props) {
await comment.delete();
onSubmit?.();
} catch (err) {
showToast(err.message, { type: "error" });
toast.error(err.message);
}
};

View File

@@ -1,10 +1,10 @@
import { observer } from "mobx-react";
import * as React from "react";
import { toast } from "sonner";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
type Props = {
/** Callback when the dialog is submitted */
@@ -30,7 +30,6 @@ const ConfirmationDialog: React.FC<Props> = ({
}: Props) => {
const [isSaving, setIsSaving] = React.useState(false);
const { dialogs } = useStores();
const { showToast } = useToasts();
const handleSubmit = React.useCallback(
async (ev: React.SyntheticEvent) => {
@@ -40,14 +39,12 @@ const ConfirmationDialog: React.FC<Props> = ({
await onSubmit();
dialogs.closeAllModals();
} catch (err) {
showToast(err.message, {
type: "error",
});
toast.error(err.message);
} finally {
setIsSaving(false);
}
},
[onSubmit, dialogs, showToast]
[onSubmit, dialogs]
);
return (

View File

@@ -1,13 +1,13 @@
import { HomeIcon } from "outline-icons";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Optional } from "utility-types";
import Flex from "~/components/Flex";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import InputSelect from "~/components/InputSelect";
import { IconWrapper } from "~/components/Sidebar/components/SidebarLink";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
type DefaultCollectionInputSelectProps = Optional<
React.ComponentProps<typeof InputSelect>
@@ -25,7 +25,6 @@ const DefaultCollectionInputSelect = ({
const { collections } = useStores();
const [fetching, setFetching] = useState(false);
const [fetchError, setFetchError] = useState();
const { showToast } = useToasts();
React.useEffect(() => {
async function fetchData() {
@@ -36,11 +35,8 @@ const DefaultCollectionInputSelect = ({
limit: 100,
});
} catch (error) {
showToast(
t("Collections could not be loaded, please reload the app"),
{
type: "error",
}
toast.error(
t("Collections could not be loaded, please reload the app")
);
setFetchError(error);
} finally {
@@ -49,7 +45,7 @@ const DefaultCollectionInputSelect = ({
}
}
void fetchData();
}, [showToast, fetchError, t, fetching, collections]);
}, [fetchError, t, fetching, collections]);
const options = React.useMemo(
() =>

View File

@@ -1,10 +1,10 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { toast } from "sonner";
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
import { useDesktopTitlebar } from "~/hooks/useDesktopTitlebar";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import Desktop from "~/utils/Desktop";
export default function DesktopEventHandler() {
@@ -12,7 +12,6 @@ export default function DesktopEventHandler() {
const { t } = useTranslation();
const history = useHistory();
const { dialogs } = useStores();
const { showToast } = useToasts();
React.useEffect(() => {
Desktop.bridge?.redirect((path: string, replace = false) => {
@@ -24,11 +23,11 @@ export default function DesktopEventHandler() {
});
Desktop.bridge?.updateDownloaded(() => {
showToast("An update is ready to install.", {
type: "info",
timeout: Infinity,
toast.message("An update is ready to install.", {
duration: Infinity,
dismissible: true,
action: {
text: "Install now",
label: t("Install now"),
onClick: () => {
void Desktop.bridge?.restartAndInstall();
},
@@ -50,7 +49,7 @@ export default function DesktopEventHandler() {
content: <KeyboardShortcuts />,
});
});
}, [t, history, dialogs, showToast]);
}, [t, history, dialogs]);
return null;
}

View File

@@ -3,9 +3,9 @@ import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { useHistory } from "react-router-dom";
import { toast } from "sonner";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import { documentPath } from "~/utils/routeHelpers";
type Props = {
@@ -14,7 +14,6 @@ type Props = {
function DocumentTemplatizeDialog({ documentId }: Props) {
const history = useHistory();
const { showToast } = useToasts();
const { t } = useTranslation();
const { documents } = useStores();
const document = documents.get(documentId);
@@ -24,11 +23,9 @@ function DocumentTemplatizeDialog({ documentId }: Props) {
const template = await document?.templatize();
if (template) {
history.push(documentPath(template));
showToast(t("Template created, go ahead and customize it"), {
type: "info",
});
toast.message(t("Template created, go ahead and customize it"));
}
}, [document, showToast, history, t]);
}, [document, history, t]);
return (
<ConfirmationDialog

View File

@@ -24,7 +24,6 @@ import type { Props as EditorProps, Editor as SharedEditor } from "~/editor";
import useDictionary from "~/hooks/useDictionary";
import useEmbeds from "~/hooks/useEmbeds";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import useUserLocale from "~/hooks/useUserLocale";
import { NotFoundError } from "~/utils/errors";
import { uploadFile } from "~/utils/files";
@@ -43,7 +42,6 @@ export type Props = Optional<
| "onClickLink"
| "embeds"
| "dictionary"
| "onShowToast"
| "extensions"
> & {
shareId?: string | undefined;
@@ -68,7 +66,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const userLocale = useUserLocale();
const locale = dateLocale(userLocale);
const { auth, comments, documents } = useStores();
const { showToast } = useToasts();
const dictionary = useDictionary();
const embeds = useEmbeds(!shareId);
const history = useHistory();
@@ -241,7 +238,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
uploadFile: handleUploadFile,
onFileUploadStart: props.onFileUploadStart,
onFileUploadStop: props.onFileUploadStop,
onShowToast: showToast,
dictionary,
isAttachment,
});
@@ -252,7 +248,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
props.onFileUploadStop,
dictionary,
handleUploadFile,
showToast,
]
);
@@ -336,7 +331,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
<LazyLoadedEditor
ref={mergeRefs([ref, localRef, handleRefChanged])}
uploadFile={handleUploadFile}
onShowToast={showToast}
embeds={embeds}
userPreferences={preferences}
dictionary={dictionary}

View File

@@ -1,6 +1,7 @@
import { observer } from "mobx-react";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { FileOperationFormat, NotificationEventType } from "@shared/types";
import Collection from "~/models/Collection";
@@ -10,7 +11,6 @@ import Text from "~/components/Text";
import env from "~/env";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import history from "~/utils/history";
import { settingsPath } from "~/utils/routeHelpers";
@@ -26,7 +26,6 @@ function ExportDialog({ collection, onSubmit }: Props) {
const [includeAttachments, setIncludeAttachments] =
React.useState<boolean>(true);
const user = useCurrentUser();
const { showToast } = useToasts();
const { collections } = useStores();
const { t } = useTranslation();
const appName = env.APP_NAME;
@@ -48,23 +47,20 @@ function ExportDialog({ collection, onSubmit }: Props) {
const handleSubmit = async () => {
if (collection) {
await collection.export(format, includeAttachments);
showToast(
t(`Your file will be available in {{ location }} soon`, {
toast.success(t("Export started"), {
description: t(`Your file will be available in {{ location }} soon`, {
location: `"${t("Settings")} > ${t("Export")}"`,
}),
{
type: "success",
action: {
text: t("Go to exports"),
onClick: () => {
history.push(settingsPath("export"));
},
action: {
label: t("View"),
onClick: () => {
history.push(settingsPath("export"));
},
}
);
},
});
} else {
await collections.export(format, includeAttachments);
showToast(t("Export started"), { type: "success" });
toast.success(t("Export started"));
}
onSubmit();
};

View File

@@ -3,24 +3,21 @@ import { ArchiveIcon } from "outline-icons";
import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import { archivePath } from "~/utils/routeHelpers";
import SidebarLink, { DragObject } from "./SidebarLink";
function ArchiveLink() {
const { policies, documents } = useStores();
const { t } = useTranslation();
const { showToast } = useToasts();
const [{ isDocumentDropping }, dropToArchiveDocument] = useDrop({
accept: "document",
drop: async (item: DragObject) => {
const document = documents.get(item.id);
await document?.archive();
showToast(t("Document archived"), {
type: "success",
});
toast.success(t("Document archived"));
},
canDrop: (item) => policies.abilities(item.id).archive,
collect: (monitor) => ({

View File

@@ -2,6 +2,7 @@ import { observer } from "mobx-react";
import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
@@ -9,7 +10,6 @@ import DocumentsLoader from "~/components/DocumentsLoader";
import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import DocumentLink from "./DocumentLink";
import DropCursor from "./DropCursor";
import EmptyCollectionPlaceholder from "./EmptyCollectionPlaceholder";
@@ -30,7 +30,6 @@ function CollectionLinkChildren({
prefetchDocument,
}: Props) {
const can = usePolicy(collection);
const { showToast } = useToasts();
const manualSort = collection.sort.field === "index";
const { documents } = useStores();
const { t } = useTranslation();
@@ -42,14 +41,10 @@ function CollectionLinkChildren({
accept: "document",
drop: (item: DragObject) => {
if (!manualSort && item.collectionId === collection?.id) {
showToast(
toast.message(
t(
"You can't reorder documents in an alphabetically sorted collection"
),
{
type: "info",
timeout: 5000,
}
)
);
return;
}

View File

@@ -6,6 +6,7 @@ import { useDrag, useDrop } from "react-dnd";
import { getEmptyImage } from "react-dnd-html5-backend";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { toast } from "sonner";
import styled from "styled-components";
import { NavigationNode } from "@shared/types";
import { sortNavigationNodes } from "@shared/utils/collections";
@@ -18,7 +19,6 @@ import Tooltip from "~/components/Tooltip";
import useBoolean from "~/hooks/useBoolean";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import DocumentMenu from "~/menus/DocumentMenu";
import { newDocumentPath } from "~/utils/routeHelpers";
import DropCursor from "./DropCursor";
@@ -53,7 +53,6 @@ function InnerDocumentLink(
}: Props,
ref: React.RefObject<HTMLAnchorElement>
) {
const { showToast } = useToasts();
const { documents, policies } = useStores();
const { t } = useTranslation();
const canUpdate = usePolicy(node.id).update;
@@ -222,14 +221,10 @@ function InnerDocumentLink(
accept: "document",
drop: (item: DragObject) => {
if (!manualSort) {
showToast(
toast.message(
t(
"You can't reorder documents in an alphabetically sorted collection"
),
{
type: "info",
timeout: 5000,
}
)
);
return;
}

View File

@@ -3,12 +3,12 @@ import { observer } from "mobx-react";
import * as React from "react";
import Dropzone from "react-dropzone";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled, { css } from "styled-components";
import LoadingIndicator from "~/components/LoadingIndicator";
import useImportDocument from "~/hooks/useImportDocument";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
type Props = {
children: JSX.Element;
@@ -21,7 +21,6 @@ type Props = {
function DropToImport({ disabled, children, collectionId, documentId }: Props) {
const { t } = useTranslation();
const { documents } = useStores();
const { showToast } = useToasts();
const { handleFiles, isImporting } = useImportDocument(
collectionId,
documentId
@@ -35,13 +34,10 @@ function DropToImport({ disabled, children, collectionId, documentId }: Props) {
const canDocument = usePolicy(documentId);
const handleRejection = React.useCallback(() => {
showToast(
t("Document not supported try Markdown, Plain text, HTML, or Word"),
{
type: "error",
}
toast.error(
t("Document not supported try Markdown, Plain text, HTML, or Word")
);
}, [t, showToast]);
}, [t]);
if (
disabled ||

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { toast } from "sonner";
import styled from "styled-components";
import { s } from "@shared/styles";
import useToasts from "~/hooks/useToasts";
type Props = {
onSubmit: (title: string) => Promise<void>;
@@ -22,7 +22,6 @@ function EditableTitle(
const [isEditing, setIsEditing] = React.useState(false);
const [originalValue, setOriginalValue] = React.useState(title);
const [value, setValue] = React.useState(title);
const { showToast } = useToasts();
React.useImperativeHandle(ref, () => ({
setIsEditing,
@@ -78,14 +77,12 @@ function EditableTitle(
setOriginalValue(trimmedValue);
} catch (error) {
setValue(originalValue);
showToast(error.message, {
type: "error",
});
toast.error(error.message);
throw error;
}
}
},
[originalValue, showToast, value, onSubmit]
[originalValue, value, onSubmit]
);
React.useEffect(() => {

View File

@@ -1,120 +0,0 @@
import { CheckboxIcon, InfoIcon, WarningIcon } from "outline-icons";
import { darken } from "polished";
import * as React from "react";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import { fadeAndScaleIn, pulse } from "~/styles/animations";
import { Toast as TToast } from "~/types";
import Spinner from "./Spinner";
type Props = {
onRequestClose: () => void;
closeAfterMs?: number;
toast: TToast;
};
function Toast({ closeAfterMs = 3000, onRequestClose, toast }: Props) {
const timeout = React.useRef<ReturnType<typeof setTimeout>>();
const [pulse, setPulse] = React.useState(false);
const { action, type = "info", reoccurring } = toast;
React.useEffect(() => {
if (toast.timeout !== 0) {
timeout.current = setTimeout(
onRequestClose,
toast.timeout || closeAfterMs
);
}
return () => timeout.current && clearTimeout(timeout.current);
}, [onRequestClose, toast, closeAfterMs]);
React.useEffect(() => {
if (reoccurring) {
setPulse(!!reoccurring);
// must match animation time in css below vvv
setTimeout(() => setPulse(false), 250);
}
}, [reoccurring]);
const handlePause = React.useCallback(() => {
if (timeout.current) {
clearTimeout(timeout.current);
}
}, []);
const handleResume = React.useCallback(() => {
if (timeout.current && toast.timeout !== 0) {
timeout.current = setTimeout(
onRequestClose,
toast.timeout || closeAfterMs
);
}
}, [onRequestClose, toast, closeAfterMs]);
return (
<ListItem
$pulse={pulse}
onMouseEnter={handlePause}
onMouseLeave={handleResume}
>
<Container onClick={action ? undefined : onRequestClose}>
{type === "loading" && <Spinner />}
{type === "info" && <InfoIcon />}
{type === "success" && <CheckboxIcon checked />}
{(type === "warning" || type === "error") && <WarningIcon />}
<Message>{toast.message}</Message>
{action && <Action onClick={action.onClick}>{action.text}</Action>}
</Container>
</ListItem>
);
}
const Action = styled.span`
display: inline-block;
padding: 4px 8px;
color: ${s("toastText")};
background: ${(props) => darken(0.05, props.theme.toastBackground)};
border-radius: 4px;
margin-left: 8px;
margin-right: -4px;
font-weight: 500;
user-select: none;
&:hover {
background: ${(props) => darken(0.1, props.theme.toastBackground)};
}
`;
const ListItem = styled.li<{ $pulse?: boolean }>`
${(props) =>
props.$pulse &&
css`
animation: ${pulse} 250ms;
`}
`;
const Container = styled.div`
display: inline-flex;
align-items: center;
animation: ${fadeAndScaleIn} 100ms ease;
margin: 8px 0;
padding: 0 12px;
color: ${s("toastText")};
background: ${s("toastBackground")};
font-size: 15px;
border-radius: 5px;
cursor: default;
&:hover {
background: ${(props) => darken(0.05, props.theme.toastBackground)};
}
`;
const Message = styled.div`
display: inline-block;
font-weight: 500;
padding: 10px 4px;
user-select: none;
`;
export default Toast;

View File

@@ -1,34 +1,28 @@
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import { depths } from "@shared/styles";
import Toast from "~/components/Toast";
import { Toaster } from "sonner";
import { useTheme } from "styled-components";
import useStores from "~/hooks/useStores";
import { Toast as TToast } from "~/types";
function Toasts() {
const { toasts } = useStores();
const { ui } = useStores();
const theme = useTheme();
return (
<List>
{toasts.orderedData.map((toast: TToast) => (
<Toast
key={toast.id}
toast={toast}
onRequestClose={() => toasts.hideToast(toast.id)}
/>
))}
</List>
<Toaster
theme={ui.resolvedTheme}
toastOptions={{
duration: 5000,
style: {
color: theme.toastText,
background: theme.toastBackground,
border: `1px solid ${theme.divider}`,
fontFamily: theme.fontFamily,
fontSize: "14px",
},
}}
/>
);
}
const List = styled.ol`
position: fixed;
left: 16px;
bottom: 16px;
list-style: none;
margin: 0;
padding: 0;
z-index: ${depths.toasts};
`;
export default observer(Toasts);

View File

@@ -4,6 +4,7 @@ import { action, observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { io, Socket } from "socket.io-client";
import { toast } from "sonner";
import RootStore from "~/stores/RootStore";
import Collection from "~/models/Collection";
import Comment from "~/models/Comment";
@@ -77,7 +78,6 @@ class WebsocketProvider extends React.Component<Props> {
this.socket.authenticated = false;
const {
auth,
toasts,
documents,
collections,
groups,
@@ -111,9 +111,7 @@ class WebsocketProvider extends React.Component<Props> {
if (this.socket) {
this.socket.authenticated = false;
}
toasts.showToast(err.message, {
type: "error",
});
toast.error(err.message);
throw err;
});