From ef76405bd6799aedc52e02826b636a201ff30c40 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 22 Oct 2023 17:30:24 -0400 Subject: [PATCH] Move toasts to sonner (#6053) --- app/actions/definitions/developer.tsx | 21 +-- app/actions/definitions/documents.tsx | 53 +++----- app/actions/definitions/revisions.tsx | 5 +- app/actions/index.ts | 5 +- app/components/Avatar/Avatar.tsx | 1 + app/components/CollectionDeleteDialog.tsx | 5 +- app/components/CollectionDescription.tsx | 11 +- app/components/CommentDeleteDialog.tsx | 5 +- app/components/ConfirmationDialog.tsx | 9 +- .../DefaultCollectionInputSelect.tsx | 12 +- app/components/DesktopEventHandler.tsx | 13 +- app/components/DocumentTemplatizeDialog.tsx | 9 +- app/components/Editor.tsx | 6 - app/components/ExportDialog.tsx | 24 ++-- .../Sidebar/components/ArchiveLink.tsx | 7 +- .../components/CollectionLinkChildren.tsx | 11 +- .../Sidebar/components/DocumentLink.tsx | 11 +- .../Sidebar/components/DropToImport.tsx | 12 +- .../Sidebar/components/EditableTitle.tsx | 9 +- app/components/Toast.tsx | 120 ------------------ app/components/Toasts.tsx | 42 +++--- app/components/WebsocketProvider.tsx | 6 +- app/editor/components/LinkEditor.tsx | 5 +- app/editor/components/LinkToolbar.tsx | 6 +- app/editor/components/SelectionToolbar.tsx | 4 - app/editor/components/SuggestionsMenu.tsx | 6 +- app/editor/index.tsx | 2 - app/hooks/useImportDocument.ts | 9 +- app/hooks/useQueryNotices.ts | 7 +- app/hooks/useToasts.ts | 9 -- app/menus/CollectionMenu.tsx | 9 +- app/menus/CommentMenu.tsx | 7 +- app/menus/DocumentMenu.tsx | 15 +-- app/menus/ShareMenu.tsx | 19 +-- app/menus/UserMenu.tsx | 14 +- app/models/GroupMembership.ts | 2 + app/scenes/APITokenNew.tsx | 15 +-- app/scenes/Collection/DropToImport.tsx | 12 +- app/scenes/CollectionEdit.tsx | 13 +- app/scenes/CollectionNew.tsx | 5 +- .../AddGroupsToCollection.tsx | 15 +-- .../AddPeopleToCollection.tsx | 13 +- app/scenes/CollectionPermissions/index.tsx | 75 ++++------- .../Document/components/CommentForm.tsx | 7 +- .../Document/components/CommentThreadItem.tsx | 5 +- app/scenes/Document/components/Document.tsx | 9 +- .../Document/components/MultiplayerEditor.tsx | 14 +- .../Document/components/SharePopover.tsx | 21 +-- app/scenes/DocumentDelete.tsx | 15 +-- app/scenes/DocumentMove.tsx | 15 +-- app/scenes/DocumentNew.tsx | 7 +- app/scenes/DocumentPermanentDelete.tsx | 13 +- app/scenes/DocumentPublish.tsx | 15 +-- app/scenes/DocumentReparent.tsx | 13 +- app/scenes/GroupDelete.tsx | 7 +- app/scenes/GroupEdit.tsx | 9 +- app/scenes/GroupMembers/AddPeopleToGroup.tsx | 13 +- app/scenes/GroupMembers/GroupMembers.tsx | 14 +- app/scenes/GroupNew.tsx | 7 +- app/scenes/Invite.tsx | 32 ++--- app/scenes/Settings/Details.tsx | 20 +-- app/scenes/Settings/Features.tsx | 7 +- app/scenes/Settings/GoogleAnalytics.tsx | 13 +- app/scenes/Settings/Notifications.tsx | 7 +- app/scenes/Settings/Preferences.tsx | 11 +- app/scenes/Settings/Profile.tsx | 19 +-- app/scenes/Settings/Security.tsx | 15 +-- app/scenes/Settings/SelfHosted.tsx | 13 +- .../Settings/components/ApiKeyListItem.tsx | 9 +- .../Settings/components/DomainManagement.tsx | 9 +- .../Settings/components/DropToImport.tsx | 25 ++-- .../components/FileOperationListItem.tsx | 13 +- app/scenes/TeamDelete.tsx | 15 +-- app/scenes/TeamNew.tsx | 7 +- app/scenes/UserDelete.tsx | 19 +-- app/stores/RootStore.ts | 3 - app/stores/ToastsStore.test.ts | 25 ---- app/stores/ToastsStore.ts | 55 -------- app/types.ts | 22 ---- package.json | 1 + .../slack/client/components/SlackListItem.tsx | 7 +- .../components/WebhookSubscriptionEdit.tsx | 15 +-- .../components/WebhookSubscriptionNew.tsx | 15 +-- shared/editor/commands/createAndInsertLink.ts | 6 +- shared/editor/commands/insertFiles.ts | 14 +- shared/editor/marks/Link.tsx | 13 +- shared/editor/nodes/CodeFence.ts | 4 +- shared/editor/nodes/Heading.ts | 3 +- shared/editor/nodes/SimpleImage.tsx | 3 +- shared/i18n/locales/en_US/translation.json | 5 +- shared/styles/theme.ts | 8 +- yarn.lock | 102 +++------------ 92 files changed, 363 insertions(+), 1015 deletions(-) delete mode 100644 app/components/Toast.tsx delete mode 100644 app/hooks/useToasts.ts delete mode 100644 app/stores/ToastsStore.test.ts delete mode 100644 app/stores/ToastsStore.ts diff --git a/app/actions/definitions/developer.tsx b/app/actions/definitions/developer.tsx index 5466e13f97..9831f78ea3 100644 --- a/app/actions/definitions/developer.tsx +++ b/app/actions/definitions/developer.tsx @@ -1,6 +1,6 @@ import { ToolsIcon, TrashIcon, UserIcon } from "outline-icons"; import * as React from "react"; -import stores from "~/stores"; +import { toast } from "sonner"; import { createAction } from "~/actions"; import { DeveloperSection } from "~/actions/sections"; import env from "~/env"; @@ -15,7 +15,7 @@ export const clearIndexedDB = createAction({ section: DeveloperSection, perform: async ({ t }) => { await deleteAllDatabases(); - stores.toasts.showToast(t("IndexedDB cache deleted")); + toast.message(t("IndexedDB cache deleted")); }, }); @@ -29,9 +29,9 @@ export const createTestUsers = createAction({ try { await client.post("/developer.create_test_users", { count }); - stores.toasts.showToast(`${count} test users created`); + toast.message(`${count} test users created`); } catch (err) { - stores.toasts.showToast(err.message, { type: "error" }); + toast.error(err.message); } }, }); @@ -41,15 +41,8 @@ export const createToast = createAction({ section: DeveloperSection, visible: () => env.ENVIRONMENT === "development", perform: async () => { - stores.toasts.showToast("Hello world", { - type: "info", - timeout: 30000, - action: { - text: "Click me", - onClick: () => { - stores.toasts.showToast("Clicked!"); - }, - }, + toast.message("Hello world", { + duration: 30000, }); }, }); @@ -60,7 +53,7 @@ export const toggleDebugLogging = createAction({ section: DeveloperSection, perform: async ({ t }) => { Logger.debugLoggingEnabled = !Logger.debugLoggingEnabled; - stores.toasts.showToast( + toast.message( Logger.debugLoggingEnabled ? t("Debug logging enabled") : t("Debug logging disabled") diff --git a/app/actions/definitions/documents.tsx b/app/actions/definitions/documents.tsx index 00e8871c1b..17d88345b1 100644 --- a/app/actions/definitions/documents.tsx +++ b/app/actions/definitions/documents.tsx @@ -25,6 +25,7 @@ import { CommentIcon, } from "outline-icons"; import * as React from "react"; +import { toast } from "sonner"; import { ExportContentType, TeamPreference } from "@shared/types"; import { getEventFiles } from "@shared/utils/files"; import DocumentDelete from "~/scenes/DocumentDelete"; @@ -209,13 +210,10 @@ export const publishDocument = createAction({ await document.save(undefined, { publish: true, }); - stores.toasts.showToast( + toast.success( t("Published {{ documentName }}", { documentName: document.noun, - }), - { - type: "success", - } + }) ); } else if (document) { stores.dialogs.openModal({ @@ -250,13 +248,10 @@ export const unpublishDocument = createAction({ await document.unpublish(); - stores.toasts.showToast( + toast.message( t("Unpublished {{ documentName }}", { documentName: document.noun, - }), - { - type: "success", - } + }) ); }, }); @@ -287,9 +282,7 @@ export const subscribeDocument = createAction({ await document?.subscribe(); - stores.toasts.showToast(t("Subscribed to document notifications"), { - type: "success", - }); + toast.success(t("Subscribed to document notifications")); }, }); @@ -319,9 +312,7 @@ export const unsubscribeDocument = createAction({ await document?.unsubscribe(currentUserId); - stores.toasts.showToast(t("Unsubscribed from document notifications"), { - type: "success", - }); + toast.success(t("Unsubscribed from document notifications")); }, }); @@ -360,15 +351,11 @@ export const downloadDocumentAsPDF = createAction({ return; } - const id = stores.toasts.showToast(`${t("Exporting")}…`, { - type: "loading", - timeout: 30 * 1000, - }); - + const id = toast.loading(`${t("Exporting")}…`); const document = stores.documents.get(activeDocumentId); document ?.download(ExportContentType.Pdf) - .finally(() => id && stores.toasts.hideToast(id)); + .finally(() => id && toast.dismiss(id)); }, }); @@ -479,12 +466,10 @@ export const pinDocumentToCollection = createAction({ const collection = stores.collections.get(activeCollectionId); if (!collection || !location.pathname.startsWith(collection?.url)) { - stores.toasts.showToast(t("Pinned to collection")); + toast.success(t("Pinned to collection")); } } catch (err) { - stores.toasts.showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }, }); @@ -521,12 +506,10 @@ export const pinDocumentToHome = createAction({ await document?.pin(); if (location.pathname !== homePath()) { - stores.toasts.showToast(t("Pinned to home")); + toast.success(t("Pinned to home")); } } catch (err) { - stores.toasts.showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }, }); @@ -569,7 +552,7 @@ export const importDocument = createAction({ return false; }, perform: ({ activeCollectionId, activeDocumentId, stores }) => { - const { documents, toasts } = stores; + const { documents } = stores; const input = document.createElement("input"); input.type = "file"; input.accept = documents.importFileTypes.join(", "); @@ -589,9 +572,7 @@ export const importDocument = createAction({ ); history.push(document.url); } catch (err) { - toasts.showToast(err.message, { - type: "error", - }); + toast.error(err.message); throw err; } }; @@ -712,9 +693,7 @@ export const archiveDocument = createAction({ } await document.archive(); - stores.toasts.showToast(t("Document archived"), { - type: "success", - }); + toast.success(t("Document archived")); } }, }); diff --git a/app/actions/definitions/revisions.tsx b/app/actions/definitions/revisions.tsx index 4ecb1bc03b..08084ab46b 100644 --- a/app/actions/definitions/revisions.tsx +++ b/app/actions/definitions/revisions.tsx @@ -2,6 +2,7 @@ import copy from "copy-to-clipboard"; import { LinkIcon, RestoreIcon } from "outline-icons"; import * as React from "react"; import { matchPath } from "react-router-dom"; +import { toast } from "sonner"; import stores from "~/stores"; import { createAction } from "~/actions"; import { RevisionSection } from "~/actions/sections"; @@ -68,9 +69,7 @@ export const copyLinkToRevision = createAction({ copy(url, { format: "text/plain", onCopy: () => { - stores.toasts.showToast(t("Link copied"), { - type: "info", - }); + toast.message(t("Link copied")); }, }); }, diff --git a/app/actions/index.ts b/app/actions/index.ts index e0762a5b99..dcc4f36e33 100644 --- a/app/actions/index.ts +++ b/app/actions/index.ts @@ -1,5 +1,6 @@ import flattenDeep from "lodash/flattenDeep"; import * as React from "react"; +import { toast } from "sonner"; import { Optional } from "utility-types"; import { v4 as uuidv4 } from "uuid"; import { @@ -77,9 +78,7 @@ export function actionToMenuItem( try { action.perform?.(context); } catch (err) { - context.stores.toasts.showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }, selected: action.selected?.(context), diff --git a/app/components/Avatar/Avatar.tsx b/app/components/Avatar/Avatar.tsx index b9da10aff2..8e73c595f0 100644 --- a/app/components/Avatar/Avatar.tsx +++ b/app/components/Avatar/Avatar.tsx @@ -5,6 +5,7 @@ import Initials from "./Initials"; export enum AvatarSize { Small = 16, + Toast = 18, Medium = 24, Large = 32, XLarge = 48, diff --git a/app/components/CollectionDeleteDialog.tsx b/app/components/CollectionDeleteDialog.tsx index 1468faa1ff..78bcf9a982 100644 --- a/app/components/CollectionDeleteDialog.tsx +++ b/app/components/CollectionDeleteDialog.tsx @@ -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 ( diff --git a/app/components/CollectionDescription.tsx b/app/components/CollectionDescription.tsx index b5e98e2d96..d0d0e22605 100644 --- a/app/components/CollectionDescription.tsx +++ b/app/components/CollectionDescription.tsx @@ -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( diff --git a/app/components/CommentDeleteDialog.tsx b/app/components/CommentDeleteDialog.tsx index 18750b29e5..5e1e902859 100644 --- a/app/components/CommentDeleteDialog.tsx +++ b/app/components/CommentDeleteDialog.tsx @@ -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); } }; diff --git a/app/components/ConfirmationDialog.tsx b/app/components/ConfirmationDialog.tsx index fcd9c23215..70282781c5 100644 --- a/app/components/ConfirmationDialog.tsx +++ b/app/components/ConfirmationDialog.tsx @@ -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) => { 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 = ({ await onSubmit(); dialogs.closeAllModals(); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { setIsSaving(false); } }, - [onSubmit, dialogs, showToast] + [onSubmit, dialogs] ); return ( diff --git a/app/components/DefaultCollectionInputSelect.tsx b/app/components/DefaultCollectionInputSelect.tsx index 68afca6590..a20c044b32 100644 --- a/app/components/DefaultCollectionInputSelect.tsx +++ b/app/components/DefaultCollectionInputSelect.tsx @@ -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 @@ -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( () => diff --git a/app/components/DesktopEventHandler.tsx b/app/components/DesktopEventHandler.tsx index 0bfeb0d3b7..e75e26ed71 100644 --- a/app/components/DesktopEventHandler.tsx +++ b/app/components/DesktopEventHandler.tsx @@ -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: , }); }); - }, [t, history, dialogs, showToast]); + }, [t, history, dialogs]); return null; } diff --git a/app/components/DocumentTemplatizeDialog.tsx b/app/components/DocumentTemplatizeDialog.tsx index d83338de63..3145673235 100644 --- a/app/components/DocumentTemplatizeDialog.tsx +++ b/app/components/DocumentTemplatizeDialog.tsx @@ -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 ( & { shareId?: string | undefined; @@ -68,7 +66,6 @@ function Editor(props: Props, ref: React.RefObject | 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 | null) { uploadFile: handleUploadFile, onFileUploadStart: props.onFileUploadStart, onFileUploadStop: props.onFileUploadStop, - onShowToast: showToast, dictionary, isAttachment, }); @@ -252,7 +248,6 @@ function Editor(props: Props, ref: React.RefObject | null) { props.onFileUploadStop, dictionary, handleUploadFile, - showToast, ] ); @@ -336,7 +331,6 @@ function Editor(props: Props, ref: React.RefObject | null) { (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(); }; diff --git a/app/components/Sidebar/components/ArchiveLink.tsx b/app/components/Sidebar/components/ArchiveLink.tsx index 17f6b04948..cfa44b54e2 100644 --- a/app/components/Sidebar/components/ArchiveLink.tsx +++ b/app/components/Sidebar/components/ArchiveLink.tsx @@ -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) => ({ diff --git a/app/components/Sidebar/components/CollectionLinkChildren.tsx b/app/components/Sidebar/components/CollectionLinkChildren.tsx index dcf1d8a6c0..ace232dc2b 100644 --- a/app/components/Sidebar/components/CollectionLinkChildren.tsx +++ b/app/components/Sidebar/components/CollectionLinkChildren.tsx @@ -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; } diff --git a/app/components/Sidebar/components/DocumentLink.tsx b/app/components/Sidebar/components/DocumentLink.tsx index 3999c910a2..70975d6954 100644 --- a/app/components/Sidebar/components/DocumentLink.tsx +++ b/app/components/Sidebar/components/DocumentLink.tsx @@ -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 ) { - 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; } diff --git a/app/components/Sidebar/components/DropToImport.tsx b/app/components/Sidebar/components/DropToImport.tsx index 55bbc6960f..01dd3439c1 100644 --- a/app/components/Sidebar/components/DropToImport.tsx +++ b/app/components/Sidebar/components/DropToImport.tsx @@ -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 || diff --git a/app/components/Sidebar/components/EditableTitle.tsx b/app/components/Sidebar/components/EditableTitle.tsx index c59a2d2fa2..dc05bdda88 100644 --- a/app/components/Sidebar/components/EditableTitle.tsx +++ b/app/components/Sidebar/components/EditableTitle.tsx @@ -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; @@ -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(() => { diff --git a/app/components/Toast.tsx b/app/components/Toast.tsx deleted file mode 100644 index 42f940c538..0000000000 --- a/app/components/Toast.tsx +++ /dev/null @@ -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>(); - 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 ( - - - {type === "loading" && } - {type === "info" && } - {type === "success" && } - {(type === "warning" || type === "error") && } - {toast.message} - {action && {action.text}} - - - ); -} - -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; diff --git a/app/components/Toasts.tsx b/app/components/Toasts.tsx index 8f4f8eb675..29e4a1af76 100644 --- a/app/components/Toasts.tsx +++ b/app/components/Toasts.tsx @@ -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 ( - - {toasts.orderedData.map((toast: TToast) => ( - toasts.hideToast(toast.id)} - /> - ))} - + ); } -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); diff --git a/app/components/WebsocketProvider.tsx b/app/components/WebsocketProvider.tsx index 0d6ca28a4b..b47bc9e080 100644 --- a/app/components/WebsocketProvider.tsx +++ b/app/components/WebsocketProvider.tsx @@ -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 { this.socket.authenticated = false; const { auth, - toasts, documents, collections, groups, @@ -111,9 +111,7 @@ class WebsocketProvider extends React.Component { if (this.socket) { this.socket.authenticated = false; } - toasts.showToast(err.message, { - type: "error", - }); + toast.error(err.message); throw err; }); diff --git a/app/editor/components/LinkEditor.tsx b/app/editor/components/LinkEditor.tsx index df5ebd0270..43a75a3272 100644 --- a/app/editor/components/LinkEditor.tsx +++ b/app/editor/components/LinkEditor.tsx @@ -9,6 +9,7 @@ import { Mark } from "prosemirror-model"; import { Selection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import * as React from "react"; +import { toast } from "sonner"; import styled from "styled-components"; import { s, hideScrollbars } from "@shared/styles"; import { isInternalUrl, sanitizeUrl } from "@shared/utils/urls"; @@ -16,7 +17,6 @@ import Flex from "~/components/Flex"; import { ResizingHeightContainer } from "~/components/ResizingHeightContainer"; import Scrollable from "~/components/Scrollable"; import { Dictionary } from "~/hooks/useDictionary"; -import { ToastOptions } from "~/types"; import Logger from "~/utils/Logger"; import Input from "./Input"; import LinkSearchResult from "./LinkSearchResult"; @@ -47,7 +47,6 @@ type Props = { href: string, event: React.MouseEvent ) => void; - onShowToast: (message: string, options?: ToastOptions) => void; view: EditorView; }; @@ -240,7 +239,7 @@ class LinkEditor extends React.Component { try { this.props.onClickLink(this.href, event); } catch (err) { - this.props.onShowToast(this.props.dictionary.openLinkError); + toast.error(this.props.dictionary.openLinkError); } }; diff --git a/app/editor/components/LinkToolbar.tsx b/app/editor/components/LinkToolbar.tsx index e46b832135..3ab23247f7 100644 --- a/app/editor/components/LinkToolbar.tsx +++ b/app/editor/components/LinkToolbar.tsx @@ -4,7 +4,6 @@ import createAndInsertLink from "@shared/editor/commands/createAndInsertLink"; import { creatingUrlPrefix } from "@shared/utils/urls"; import useDictionary from "~/hooks/useDictionary"; import useEventListener from "~/hooks/useEventListener"; -import useToasts from "~/hooks/useToasts"; import { useEditor } from "./EditorContext"; import FloatingToolbar from "./FloatingToolbar"; import LinkEditor, { SearchResult } from "./LinkEditor"; @@ -39,7 +38,6 @@ export default function LinkToolbar({ }: Props) { const dictionary = useDictionary(); const { view } = useEditor(); - const { showToast } = useToasts(); const menuRef = React.useRef(null); useEventListener("mousedown", (event: Event) => { @@ -84,11 +82,10 @@ export default function LinkToolbar({ return createAndInsertLink(view, title, href, { onCreateLink, - onShowToast: showToast, dictionary, }); }, - [onCreateLink, onClose, view, dictionary, showToast] + [onCreateLink, onClose, view, dictionary] ); const handleOnSelectLink = React.useCallback( @@ -137,7 +134,6 @@ export default function LinkToolbar({ onCreateLink={onCreateLink ? handleOnCreateLink : undefined} onSelectLink={handleOnSelectLink} onRemoveLink={onClose} - onShowToast={showToast} onClickLink={onClickLink} onSearchLink={onSearchLink} dictionary={dictionary} diff --git a/app/editor/components/SelectionToolbar.tsx b/app/editor/components/SelectionToolbar.tsx index 3d8f824025..5798a9d744 100644 --- a/app/editor/components/SelectionToolbar.tsx +++ b/app/editor/components/SelectionToolbar.tsx @@ -15,7 +15,6 @@ import useDictionary from "~/hooks/useDictionary"; import useEventListener from "~/hooks/useEventListener"; import useMobile from "~/hooks/useMobile"; import usePrevious from "~/hooks/usePrevious"; -import useToasts from "~/hooks/useToasts"; import getCodeMenuItems from "../menus/code"; import getDividerMenuItems from "../menus/divider"; import getFormattingMenuItems from "../menus/formatting"; @@ -97,7 +96,6 @@ function useIsDragging() { export default function SelectionToolbar(props: Props) { const { onClose, readOnly, onOpen } = props; const { view, commands } = useEditor(); - const { showToast: onShowToast } = useToasts(); const dictionary = useDictionary(); const menuRef = React.useRef(null); const isActive = useIsActive(view.state); @@ -175,7 +173,6 @@ export default function SelectionToolbar(props: Props) { return createAndInsertLink(view, title, href, { onCreateLink, - onShowToast, dictionary, }); }; @@ -271,7 +268,6 @@ export default function SelectionToolbar(props: Props) { mark={range.mark} from={range.from} to={range.to} - onShowToast={onShowToast} onClickLink={props.onClickLink} onSearchLink={props.onSearchLink} onCreateLink={onCreateLink ? handleOnCreateLink : undefined} diff --git a/app/editor/components/SuggestionsMenu.tsx b/app/editor/components/SuggestionsMenu.tsx index d311008e99..1866a65a0d 100644 --- a/app/editor/components/SuggestionsMenu.tsx +++ b/app/editor/components/SuggestionsMenu.tsx @@ -3,6 +3,7 @@ import capitalize from "lodash/capitalize"; import * as React from "react"; import { Trans } from "react-i18next"; import { VisuallyHidden } from "reakit/VisuallyHidden"; +import { toast } from "sonner"; import styled from "styled-components"; import insertFiles from "@shared/editor/commands/insertFiles"; import { EmbedDescriptor } from "@shared/editor/embeds"; @@ -15,7 +16,6 @@ import { AttachmentValidation } from "@shared/validations"; import { Portal } from "~/components/Portal"; import Scrollable from "~/components/Scrollable"; import useDictionary from "~/hooks/useDictionary"; -import useToasts from "~/hooks/useToasts"; import Logger from "~/utils/Logger"; import { useEditor } from "./EditorContext"; import Input from "./Input"; @@ -77,7 +77,6 @@ export type Props = { function SuggestionsMenu(props: Props) { const { view, commands } = useEditor(); - const { showToast: onShowToast } = useToasts(); const dictionary = useDictionary(); const hasActivated = React.useRef(false); const menuRef = React.useRef(null); @@ -292,7 +291,7 @@ function SuggestionsMenu(props: Props) { const matches = "matcher" in insertItem && insertItem.matcher(href); if (!matches) { - onShowToast(dictionary.embedInvalidLink); + toast.error(dictionary.embedInvalidLink); return; } @@ -365,7 +364,6 @@ function SuggestionsMenu(props: Props) { uploadFile, onFileUploadStart, onFileUploadStop, - onShowToast, dictionary, isAttachment: inputRef.current?.accept === "*", }); diff --git a/app/editor/index.tsx b/app/editor/index.tsx index aaf7a3a22a..08bc8dcca1 100644 --- a/app/editor/index.tsx +++ b/app/editor/index.tsx @@ -133,8 +133,6 @@ export type Props = { userPreferences?: UserPreferences | null; /** Whether embeds should be rendered without an iframe */ embedsDisabled?: boolean; - /** Callback when a toast message is triggered (eg "link copied") */ - onShowToast: (message: string) => void; className?: string; /** Optional style overrides for the container*/ style?: React.CSSProperties; diff --git a/app/hooks/useImportDocument.ts b/app/hooks/useImportDocument.ts index 5c0cb46078..bcac9085db 100644 --- a/app/hooks/useImportDocument.ts +++ b/app/hooks/useImportDocument.ts @@ -2,8 +2,8 @@ import invariant from "invariant"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; +import { toast } from "sonner"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import { documentPath } from "~/utils/routeHelpers"; let importingLock = false; @@ -16,7 +16,6 @@ export default function useImportDocument( isImporting: boolean; } { const { documents } = useStores(); - const { showToast } = useToasts(); const [isImporting, setImporting] = React.useState(false); const { t } = useTranslation(); const history = useHistory(); @@ -55,15 +54,13 @@ export default function useImportDocument( } } } catch (err) { - showToast(`${t("Could not import file")}. ${err.message}`, { - type: "error", - }); + toast.error(`${t("Could not import file")}. ${err.message}`); } finally { setImporting(false); importingLock = false; } }, - [t, documents, history, showToast, collectionId, documentId] + [t, documents, history, collectionId, documentId] ); return { diff --git a/app/hooks/useQueryNotices.ts b/app/hooks/useQueryNotices.ts index 6a2e5f215b..145fc00ffe 100644 --- a/app/hooks/useQueryNotices.ts +++ b/app/hooks/useQueryNotices.ts @@ -1,8 +1,8 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { QueryNotices } from "@shared/types"; import useQuery from "./useQuery"; -import useToasts from "./useToasts"; /** * Display a toast message based on a notice in the query string. This is usually @@ -12,13 +12,12 @@ import useToasts from "./useToasts"; export default function useQueryNotices() { const query = useQuery(); const { t } = useTranslation(); - const { showToast } = useToasts(); const notice = query.get("notice") as QueryNotices; React.useEffect(() => { switch (notice) { case QueryNotices.UnsubscribeDocument: { - showToast( + toast.success( t("Unsubscribed from document", { type: "success", }) @@ -27,5 +26,5 @@ export default function useQueryNotices() { } default: } - }, [t, showToast, notice]); + }, [t, notice]); } diff --git a/app/hooks/useToasts.ts b/app/hooks/useToasts.ts deleted file mode 100644 index 4d4507eea2..0000000000 --- a/app/hooks/useToasts.ts +++ /dev/null @@ -1,9 +0,0 @@ -import useStores from "./useStores"; - -export default function useToasts() { - const { toasts } = useStores(); - return { - showToast: toasts.showToast, - hideToast: toasts.hideToast, - }; -} diff --git a/app/menus/CollectionMenu.tsx b/app/menus/CollectionMenu.tsx index fc5a79ce44..5885c69327 100644 --- a/app/menus/CollectionMenu.tsx +++ b/app/menus/CollectionMenu.tsx @@ -11,6 +11,7 @@ import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; import { useMenuState, MenuButton, MenuButtonHTMLProps } from "reakit/Menu"; import { VisuallyHidden } from "reakit/VisuallyHidden"; +import { toast } from "sonner"; import { getEventFiles } from "@shared/utils/files"; import Collection from "~/models/Collection"; import ContextMenu, { Placement } from "~/components/ContextMenu"; @@ -29,7 +30,6 @@ import useActionContext from "~/hooks/useActionContext"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import { MenuItem } from "~/types"; import { newDocumentPath } from "~/utils/routeHelpers"; @@ -56,7 +56,6 @@ function CollectionMenu({ }); const team = useCurrentTeam(); const { documents, dialogs } = useStores(); - const { showToast } = useToasts(); const { t } = useTranslation(); const history = useHistory(); const file = React.useRef(null); @@ -116,13 +115,11 @@ function CollectionMenu({ }); history.push(document.url); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); throw err; } }, - [history, showToast, collection.id, documents] + [history, collection.id, documents] ); const handleChangeSort = React.useCallback( diff --git a/app/menus/CommentMenu.tsx b/app/menus/CommentMenu.tsx index a561a80dc8..f487e0d60f 100644 --- a/app/menus/CommentMenu.tsx +++ b/app/menus/CommentMenu.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { useMenuState } from "reakit/Menu"; +import { toast } from "sonner"; import Comment from "~/models/Comment"; import CommentDeleteDialog from "~/components/CommentDeleteDialog"; import ContextMenu from "~/components/ContextMenu"; @@ -12,7 +13,6 @@ import Separator from "~/components/ContextMenu/Separator"; import EventBoundary from "~/components/EventBoundary"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import { commentPath, urlify } from "~/utils/routeHelpers"; type Props = { @@ -30,7 +30,6 @@ function CommentMenu({ comment, onEdit, onDelete, className }: Props) { const menu = useMenuState({ modal: true, }); - const { showToast } = useToasts(); const { documents, dialogs } = useStores(); const { t } = useTranslation(); const can = usePolicy(comment.id); @@ -47,9 +46,9 @@ function CommentMenu({ comment, onEdit, onDelete, className }: Props) { const handleCopyLink = React.useCallback(() => { if (document) { copy(urlify(commentPath(document, comment))); - showToast(t("Link copied")); + toast.message(t("Link copied")); } - }, [t, document, comment, showToast]); + }, [t, document, comment]); return ( <> diff --git a/app/menus/DocumentMenu.tsx b/app/menus/DocumentMenu.tsx index 47491c36ac..479a48eb50 100644 --- a/app/menus/DocumentMenu.tsx +++ b/app/menus/DocumentMenu.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; import { useMenuState, MenuButton, MenuButtonHTMLProps } from "reakit/Menu"; import { VisuallyHidden } from "reakit/VisuallyHidden"; +import { toast } from "sonner"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; import { s, ellipsis } from "@shared/styles"; @@ -47,7 +48,6 @@ import useMobile from "~/hooks/useMobile"; import usePolicy from "~/hooks/usePolicy"; import useRequest from "~/hooks/useRequest"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import { MenuItem } from "~/types"; import { documentEditPath } from "~/utils/routeHelpers"; @@ -79,7 +79,6 @@ function DocumentMenu({ }: Props) { const user = useCurrentUser(); const { policies, collections, documents, subscriptions } = useStores(); - const { showToast } = useToasts(); const menu = useMenuState({ modal, unstable_preventOverflow: true, @@ -120,11 +119,9 @@ function DocumentMenu({ } ) => { await document.restore(options); - showToast(t("Document restored"), { - type: "success", - }); + toast.success(t("Document restored")); }, - [showToast, t, document] + [t, document] ); const collection = document.collectionId @@ -187,13 +184,11 @@ function DocumentMenu({ ); history.push(importedDocument.url); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); throw err; } }, - [history, showToast, collection, documents, document.id] + [history, collection, documents, document.id] ); return ( diff --git a/app/menus/ShareMenu.tsx b/app/menus/ShareMenu.tsx index fad3235f98..17feb01639 100644 --- a/app/menus/ShareMenu.tsx +++ b/app/menus/ShareMenu.tsx @@ -4,6 +4,7 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; import { useMenuState } from "reakit/Menu"; +import { toast } from "sonner"; import Share from "~/models/Share"; import ContextMenu from "~/components/ContextMenu"; import MenuItem from "~/components/ContextMenu/MenuItem"; @@ -11,7 +12,6 @@ import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton"; import CopyToClipboard from "~/components/CopyToClipboard"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; type Props = { share: Share; @@ -22,7 +22,6 @@ function ShareMenu({ share }: Props) { modal: true, }); const { shares } = useStores(); - const { showToast } = useToasts(); const { t } = useTranslation(); const history = useHistory(); const can = usePolicy(share.id); @@ -41,23 +40,17 @@ function ShareMenu({ share }: Props) { try { await shares.revoke(share); - showToast(t("Share link revoked"), { - type: "info", - }); + toast.message(t("Share link revoked")); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }, - [t, shares, share, showToast] + [t, shares, share] ); const handleCopy = React.useCallback(() => { - showToast(t("Share link copied"), { - type: "info", - }); - }, [t, showToast]); + toast.success(t("Share link copied")); + }, [t]); return ( <> diff --git a/app/menus/UserMenu.tsx b/app/menus/UserMenu.tsx index 0f42ec84dc..11378019d7 100644 --- a/app/menus/UserMenu.tsx +++ b/app/menus/UserMenu.tsx @@ -2,6 +2,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { useMenuState } from "reakit/Menu"; +import { toast } from "sonner"; import User from "~/models/User"; import ContextMenu from "~/components/ContextMenu"; import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton"; @@ -18,7 +19,6 @@ import { deleteUserActionFactory } from "~/actions/definitions/users"; import useActionContext from "~/hooks/useActionContext"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; type Props = { user: User; @@ -31,7 +31,6 @@ function UserMenu({ user }: Props) { modal: true, }); const can = usePolicy(user.id); - const { showToast } = useToasts(); const context = useActionContext({ isContextMenu: true, }); @@ -129,17 +128,14 @@ function UserMenu({ user }: Props) { try { await users.resendInvite(user); - showToast(t(`Invite was resent to ${user.name}`), { type: "success" }); + toast.success(t(`Invite was resent to ${user.name}`)); } catch (err) { - showToast( - err.message ?? t(`An error occurred while sending the invite`), - { - type: "error", - } + toast.error( + err.message ?? t(`An error occurred while sending the invite`) ); } }, - [users, user, t, showToast] + [users, user, t] ); const handleActivate = React.useCallback( diff --git a/app/models/GroupMembership.ts b/app/models/GroupMembership.ts index 43cd19cff6..3ba10ddacd 100644 --- a/app/models/GroupMembership.ts +++ b/app/models/GroupMembership.ts @@ -1,5 +1,6 @@ import User from "./User"; import Model from "./base/Model"; +import Relation from "./decorators/Relation"; class GroupMembership extends Model { id: string; @@ -8,6 +9,7 @@ class GroupMembership extends Model { groupId: string; + @Relation(() => User, { onDelete: "cascade" }) user: User; } diff --git a/app/scenes/APITokenNew.tsx b/app/scenes/APITokenNew.tsx index c82cedd4f5..cecce47481 100644 --- a/app/scenes/APITokenNew.tsx +++ b/app/scenes/APITokenNew.tsx @@ -1,11 +1,11 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import Button from "~/components/Button"; import Flex from "~/components/Flex"; import Input from "~/components/Input"; import Text from "~/components/Text"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; type Props = { onSubmit: () => void; @@ -15,7 +15,6 @@ function APITokenNew({ onSubmit }: Props) { const [name, setName] = React.useState(""); const [isSaving, setIsSaving] = React.useState(false); const { apiKeys } = useStores(); - const { showToast } = useToasts(); const { t } = useTranslation(); const handleSubmit = React.useCallback( @@ -27,21 +26,15 @@ function APITokenNew({ onSubmit }: Props) { await apiKeys.create({ name, }); - showToast( - t("API token created", { - type: "success", - }) - ); + toast.success(t("API token created")); onSubmit(); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { setIsSaving(false); } }, - [t, showToast, name, onSubmit, apiKeys] + [t, name, onSubmit, apiKeys] ); const handleNameChange = React.useCallback((event) => { diff --git a/app/scenes/Collection/DropToImport.tsx b/app/scenes/Collection/DropToImport.tsx index 5fcf67000d..a739f1f929 100644 --- a/app/scenes/Collection/DropToImport.tsx +++ b/app/scenes/Collection/DropToImport.tsx @@ -2,11 +2,11 @@ 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 Text from "~/components/Text"; import useImportDocument from "~/hooks/useImportDocument"; -import useToasts from "~/hooks/useToasts"; type Props = { disabled: boolean; @@ -22,17 +22,13 @@ const DropToImport: React.FC = ({ collectionId, }: Props) => { const { handleFiles, isImporting } = useImportDocument(collectionId); - const { showToast } = useToasts(); const { t } = useTranslation(); 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]); return ( { direction: "asc" | "desc"; }>(collection.sort); const [isSaving, setIsSaving] = useState(false); - const { showToast } = useToasts(); const { t } = useTranslation(); const handleSubmit = React.useCallback( @@ -46,18 +45,14 @@ const CollectionEdit = ({ collectionId, onSubmit }: Props) => { sort, }); onSubmit(); - showToast(t("The collection was updated"), { - type: "success", - }); + toast.success(t("The collection was updated")); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { setIsSaving(false); } }, - [collection, color, icon, name, onSubmit, showToast, sort, t] + [collection, color, icon, name, onSubmit, sort, t] ); const handleSortChange = (value: string) => { diff --git a/app/scenes/CollectionNew.tsx b/app/scenes/CollectionNew.tsx index a5de7c4f25..8c301ff32a 100644 --- a/app/scenes/CollectionNew.tsx +++ b/app/scenes/CollectionNew.tsx @@ -3,6 +3,7 @@ import { observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import { withTranslation, Trans, WithTranslation } from "react-i18next"; +import { toast } from "sonner"; import { randomElement } from "@shared/random"; import { CollectionPermission } from "@shared/types"; import { colorPalette } from "@shared/utils/collections"; @@ -66,9 +67,7 @@ class CollectionNew extends React.Component { this.props.onSubmit(); history.push(collection.url); } catch (err) { - this.props.toasts.showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { this.isSaving = false; } diff --git a/app/scenes/CollectionPermissions/AddGroupsToCollection.tsx b/app/scenes/CollectionPermissions/AddGroupsToCollection.tsx index d78947d416..b3dd13706a 100644 --- a/app/scenes/CollectionPermissions/AddGroupsToCollection.tsx +++ b/app/scenes/CollectionPermissions/AddGroupsToCollection.tsx @@ -2,6 +2,7 @@ import debounce from "lodash/debounce"; import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import styled from "styled-components"; import Collection from "~/models/Collection"; import Group from "~/models/Group"; @@ -29,8 +30,7 @@ function AddGroupsToCollection(props: Props) { useBoolean(false); const [query, setQuery] = React.useState(""); - const { auth, collectionGroupMemberships, groups, policies, toasts } = - useStores(); + const { auth, collectionGroupMemberships, groups, policies } = useStores(); const { fetchPage: fetchGroups } = groups; const { t } = useTranslation(); @@ -55,18 +55,13 @@ function AddGroupsToCollection(props: Props) { collectionId: collection.id, groupId: group.id, }); - toasts.showToast( + toast.success( t("{{ groupName }} was added to the collection", { groupName: group.name, - }), - { - type: "success", - } + }) ); } catch (err) { - toasts.showToast(t("Could not add user"), { - type: "error", - }); + toast.error(t("Could not add user")); } }; diff --git a/app/scenes/CollectionPermissions/AddPeopleToCollection.tsx b/app/scenes/CollectionPermissions/AddPeopleToCollection.tsx index 0bb5875033..0fc48f8cf2 100644 --- a/app/scenes/CollectionPermissions/AddPeopleToCollection.tsx +++ b/app/scenes/CollectionPermissions/AddPeopleToCollection.tsx @@ -1,9 +1,12 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import Collection from "~/models/Collection"; import User from "~/models/User"; import Invite from "~/scenes/Invite"; +import Avatar from "~/components/Avatar"; +import { AvatarSize } from "~/components/Avatar/Avatar"; import ButtonLink from "~/components/ButtonLink"; import Empty from "~/components/Empty"; import Flex from "~/components/Flex"; @@ -15,7 +18,6 @@ import useBoolean from "~/hooks/useBoolean"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import useStores from "~/hooks/useStores"; import useThrottledCallback from "~/hooks/useThrottledCallback"; -import useToasts from "~/hooks/useToasts"; import MemberListItem from "./components/MemberListItem"; type Props = { @@ -24,7 +26,6 @@ type Props = { function AddPeopleToCollection({ collection }: Props) { const { memberships, users } = useStores(); - const { showToast } = useToasts(); const team = useCurrentTeam(); const { t } = useTranslation(); const [inviteModalOpen, setInviteModalOpen, setInviteModalClosed] = @@ -50,18 +51,16 @@ function AddPeopleToCollection({ collection }: Props) { collectionId: collection.id, userId: user.id, }); - showToast( + toast.success( t("{{ userName }} was added to the collection", { userName: user.name, }), { - type: "success", + icon: , } ); } catch (err) { - showToast(t("Could not add user"), { - type: "error", - }); + toast.error(t("Could not add user")); } }; diff --git a/app/scenes/CollectionPermissions/index.tsx b/app/scenes/CollectionPermissions/index.tsx index c556ddc0cc..e305f4e492 100644 --- a/app/scenes/CollectionPermissions/index.tsx +++ b/app/scenes/CollectionPermissions/index.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react"; import { PlusIcon } from "outline-icons"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import styled from "styled-components"; import { CollectionPermission } from "@shared/types"; import Group from "~/models/Group"; @@ -19,7 +20,6 @@ import Text from "~/components/Text"; import useBoolean from "~/hooks/useBoolean"; import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import AddGroupsToCollection from "./AddGroupsToCollection"; import AddPeopleToCollection from "./AddPeopleToCollection"; import CollectionGroupMemberListItem from "./components/CollectionGroupMemberListItem"; @@ -40,7 +40,6 @@ function CollectionPermissions({ collectionId }: Props) { groups, auth, } = useStores(); - const { showToast } = useToasts(); const collection = collections.get(collectionId); invariant(collection, "Collection not found"); @@ -60,21 +59,16 @@ function CollectionPermissions({ collectionId }: Props) { collectionId: collection.id, userId: user.id, }); - showToast( + toast.success( t(`{{ userName }} was removed from the collection`, { userName: user.name, - }), - { - type: "success", - } + }) ); } catch (err) { - showToast(t("Could not remove user"), { - type: "error", - }); + toast.error(t("Could not remove user")); } }, - [memberships, showToast, collection, t] + [memberships, collection, t] ); const handleUpdateUser = React.useCallback( @@ -85,21 +79,16 @@ function CollectionPermissions({ collectionId }: Props) { userId: user.id, permission, }); - showToast( + toast.success( t(`{{ userName }} permissions were updated`, { userName: user.name, - }), - { - type: "success", - } + }) ); } catch (err) { - showToast(t("Could not update user"), { - type: "error", - }); + toast.error(t("Could not update user")); } }, - [memberships, showToast, collection, t] + [memberships, collection, t] ); const handleRemoveGroup = React.useCallback( @@ -109,21 +98,16 @@ function CollectionPermissions({ collectionId }: Props) { collectionId: collection.id, groupId: group.id, }); - showToast( + toast.success( t(`The {{ groupName }} group was removed from the collection`, { groupName: group.name, - }), - { - type: "success", - } + }) ); } catch (err) { - showToast(t("Could not remove group"), { - type: "error", - }); + toast.error(t("Could not remove group")); } }, - [collectionGroupMemberships, showToast, collection, t] + [collectionGroupMemberships, collection, t] ); const handleUpdateGroup = React.useCallback( @@ -134,21 +118,16 @@ function CollectionPermissions({ collectionId }: Props) { groupId: group.id, permission, }); - showToast( + toast.success( t(`{{ groupName }} permissions were updated`, { groupName: group.name, - }), - { - type: "success", - } + }) ); } catch (err) { - showToast(t("Could not update user"), { - type: "error", - }); + toast.error(t("Could not update user")); } }, - [collectionGroupMemberships, showToast, collection, t] + [collectionGroupMemberships, collection, t] ); const handleChangePermission = React.useCallback( @@ -157,16 +136,12 @@ function CollectionPermissions({ collectionId }: Props) { await collection.save({ permission, }); - showToast(t("Default access permissions were updated"), { - type: "success", - }); + toast.success(t("Default access permissions were updated")); } catch (err) { - showToast(t("Could not update permissions"), { - type: "error", - }); + toast.error(t("Could not update permissions")); } }, - [collection, showToast, t] + [collection, t] ); const fetchOptions = React.useMemo( @@ -182,16 +157,12 @@ function CollectionPermissions({ collectionId }: Props) { await collection.save({ sharing: ev.target.checked, }); - showToast(t("Public document sharing permissions were updated"), { - type: "success", - }); + toast.success(t("Public document sharing permissions were updated")); } catch (err) { - showToast(t("Could not update public document sharing"), { - type: "error", - }); + toast.error(t("Could not update public document sharing")); } }, - [collection, showToast, t] + [collection, t] ); const collectionName = collection.name; diff --git a/app/scenes/Document/components/CommentForm.tsx b/app/scenes/Document/components/CommentForm.tsx index f44076e144..bde685bb69 100644 --- a/app/scenes/Document/components/CommentForm.tsx +++ b/app/scenes/Document/components/CommentForm.tsx @@ -3,6 +3,7 @@ import { action } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { v4 as uuidv4 } from "uuid"; import { CommentValidation } from "@shared/validations"; import Comment from "~/models/Comment"; @@ -15,7 +16,6 @@ import useCurrentUser from "~/hooks/useCurrentUser"; import useOnClickOutside from "~/hooks/useOnClickOutside"; import usePersistedState from "~/hooks/usePersistedState"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import CommentEditor from "./CommentEditor"; import { Bubble } from "./CommentThreadItem"; @@ -65,7 +65,6 @@ function CommentForm({ const [forceRender, setForceRender] = React.useState(0); const [inputFocused, setInputFocused] = React.useState(autoFocus); const { t } = useTranslation(); - const { showToast } = useToasts(); const { comments } = useStores(); const user = useCurrentUser(); @@ -106,7 +105,7 @@ function CommentForm({ }) .catch(() => { comment.isNew = true; - showToast(t("Error creating comment"), { type: "error" }); + toast.error(t("Error creating comment")); }); // optimistically update the comment model @@ -139,7 +138,7 @@ function CommentForm({ comment.save().catch(() => { comments.remove(comment.id); comment.isNew = true; - showToast(t("Error creating comment"), { type: "error" }); + toast.error(t("Error creating comment")); }); // optimistically update the comment model diff --git a/app/scenes/Document/components/CommentThreadItem.tsx b/app/scenes/Document/components/CommentThreadItem.tsx index 204ff01321..8748fe34f3 100644 --- a/app/scenes/Document/components/CommentThreadItem.tsx +++ b/app/scenes/Document/components/CommentThreadItem.tsx @@ -4,6 +4,7 @@ import { observer } from "mobx-react"; import { darken } from "polished"; import * as React from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import styled, { css } from "styled-components"; import breakpoint from "styled-components-breakpoint"; import { s } from "@shared/styles"; @@ -17,7 +18,6 @@ import Flex from "~/components/Flex"; import Text from "~/components/Text"; import Time from "~/components/Time"; import useBoolean from "~/hooks/useBoolean"; -import useToasts from "~/hooks/useToasts"; import CommentMenu from "~/menus/CommentMenu"; import { hover } from "~/styles"; import CommentEditor from "./CommentEditor"; @@ -85,7 +85,6 @@ function CommentThreadItem({ canReply, }: Props) { const { editor } = useDocumentContext(); - const { showToast } = useToasts(); const { t } = useTranslation(); const [forceRender, setForceRender] = React.useState(0); const [data, setData] = React.useState(toJS(comment.data)); @@ -116,7 +115,7 @@ function CommentThreadItem({ }); } catch (error) { setEditing(); - showToast(t("Error updating comment"), { type: "error" }); + toast.error(t("Error updating comment")); } }; diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx index 8e3542d40b..1fb3c9ba2d 100644 --- a/app/scenes/Document/components/Document.tsx +++ b/app/scenes/Document/components/Document.tsx @@ -11,6 +11,7 @@ import { withRouter, Redirect, } from "react-router"; +import { toast } from "sonner"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; import { s } from "@shared/styles"; @@ -176,7 +177,7 @@ class DocumentScene extends React.Component { }; onSynced = async () => { - const { toasts, history, location, t } = this.props; + const { history, location, t } = this.props; const restore = location.state?.restore; const revisionId = location.state?.revisionId; const editorRef = this.editor.current; @@ -191,7 +192,7 @@ class DocumentScene extends React.Component { if (response) { await this.replaceDocument(response.data); - toasts.showToast(t("Document restored")); + toast.success(t("Document restored")); history.replace(this.props.document.url, history.location.state); } }; @@ -316,9 +317,7 @@ class DocumentScene extends React.Component { this.props.ui.setActiveDocument(savedDocument); } } catch (err) { - this.props.toasts.showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { this.isSaving = false; this.isPublishing = false; diff --git a/app/scenes/Document/components/MultiplayerEditor.tsx b/app/scenes/Document/components/MultiplayerEditor.tsx index 443425b594..614fb3f462 100644 --- a/app/scenes/Document/components/MultiplayerEditor.tsx +++ b/app/scenes/Document/components/MultiplayerEditor.tsx @@ -3,6 +3,7 @@ import throttle from "lodash/throttle"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; +import { toast } from "sonner"; import { IndexeddbPersistence } from "y-indexeddb"; import * as Y from "yjs"; import MultiplayerExtension from "@shared/editor/extensions/Multiplayer"; @@ -14,7 +15,6 @@ import useIdle from "~/hooks/useIdle"; import useIsMounted from "~/hooks/useIsMounted"; import usePageVisibility from "~/hooks/usePageVisibility"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import { AwarenessChangeEvent } from "~/types"; import Logger from "~/utils/Logger"; import { homePath } from "~/utils/routeHelpers"; @@ -51,7 +51,6 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) { const [isLocalSynced, setLocalSynced] = React.useState(false); const [isRemoteSynced, setRemoteSynced] = React.useState(false); const [ydoc] = React.useState(() => new Y.Doc()); - const { showToast } = useToasts(); const token = auth.collaborationToken; const isIdle = useIdle(); const isVisible = usePageVisibility(); @@ -180,7 +179,6 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) { }; }, [ history, - showToast, t, documentId, ui, @@ -251,21 +249,17 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) { React.useEffect(() => { function onUnhandledError(event: ErrorEvent) { if (event.message.includes("URIError: URI malformed")) { - showToast( + toast.error( t( "Sorry, the last change could not be persisted – please reload the page" - ), - { - type: "error", - timeout: 0, - } + ) ); } } window.addEventListener("error", onUnhandledError); return () => window.removeEventListener("error", onUnhandledError); - }, [showToast, t]); + }, [t]); if (!remoteProvider) { return null; diff --git a/app/scenes/Document/components/SharePopover.tsx b/app/scenes/Document/components/SharePopover.tsx index a4ca2fd80c..48112639c8 100644 --- a/app/scenes/Document/components/SharePopover.tsx +++ b/app/scenes/Document/components/SharePopover.tsx @@ -6,6 +6,7 @@ import { ExpandedIcon, GlobeIcon, PadlockIcon } from "outline-icons"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; import { Link } from "react-router-dom"; +import { toast } from "sonner"; import styled from "styled-components"; import { s } from "@shared/styles"; import { dateLocale, dateToRelative } from "@shared/utils/date"; @@ -23,7 +24,6 @@ import useCurrentTeam from "~/hooks/useCurrentTeam"; import useKeyDown from "~/hooks/useKeyDown"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import useUserLocale from "~/hooks/useUserLocale"; type Props = { @@ -44,7 +44,6 @@ function SharePopover({ const team = useCurrentTeam(); const { t } = useTranslation(); const { shares, collections } = useStores(); - const { showToast } = useToasts(); const [expandedOptions, setExpandedOptions] = React.useState(false); const [isEditMode, setIsEditMode] = React.useState(false); const [slugValidationError, setSlugValidationError] = React.useState(""); @@ -100,12 +99,10 @@ function SharePopover({ published: event.currentTarget.checked, }); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }, - [document.id, shares, showToast] + [document.id, shares] ); const handleChildDocumentsChange = React.useCallback( @@ -118,22 +115,18 @@ function SharePopover({ includeChildDocuments: event.currentTarget.checked, }); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }, - [document.id, shares, showToast] + [document.id, shares] ); const handleCopied = React.useCallback(() => { timeout.current = setTimeout(() => { onRequestClose(); - showToast(t("Share link copied"), { - type: "info", - }); + toast.message(t("Share link copied")); }, 250); - }, [t, onRequestClose, showToast]); + }, [t, onRequestClose]); const handleUrlSlugChange = React.useMemo( () => diff --git a/app/scenes/DocumentDelete.tsx b/app/scenes/DocumentDelete.tsx index 1a74b678f5..28d74fbf6b 100644 --- a/app/scenes/DocumentDelete.tsx +++ b/app/scenes/DocumentDelete.tsx @@ -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 Document from "~/models/Document"; 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"; import { collectionPath, documentPath } from "~/utils/routeHelpers"; type Props = { @@ -21,7 +21,6 @@ function DocumentDelete({ document, onSubmit }: Props) { const history = useHistory(); const [isDeleting, setDeleting] = React.useState(false); const [isArchiving, setArchiving] = React.useState(false); - const { showToast } = useToasts(); const canArchive = !document.isDraft && !document.isArchived; const collection = document.collectionId ? collections.get(document.collectionId) @@ -57,14 +56,12 @@ function DocumentDelete({ document, onSubmit }: Props) { onSubmit(); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { setDeleting(false); } }, - [showToast, onSubmit, ui, document, documents, history, collection] + [onSubmit, ui, document, documents, history, collection] ); const handleArchive = React.useCallback( @@ -76,14 +73,12 @@ function DocumentDelete({ document, onSubmit }: Props) { await document.archive(); onSubmit(); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { setArchiving(false); } }, - [showToast, onSubmit, document] + [onSubmit, document] ); return ( diff --git a/app/scenes/DocumentMove.tsx b/app/scenes/DocumentMove.tsx index abd05775a0..319c7b8136 100644 --- a/app/scenes/DocumentMove.tsx +++ b/app/scenes/DocumentMove.tsx @@ -2,6 +2,7 @@ import flatten from "lodash/flatten"; import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import styled from "styled-components"; import { ellipsis } from "@shared/styles"; import { NavigationNode } from "@shared/types"; @@ -12,7 +13,6 @@ import Flex from "~/components/Flex"; import Text from "~/components/Text"; import useCollectionTrees from "~/hooks/useCollectionTrees"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import { flattenTree } from "~/utils/tree"; type Props = { @@ -21,7 +21,6 @@ type Props = { function DocumentMove({ document }: Props) { const { dialogs } = useStores(); - const { showToast } = useToasts(); const { t } = useTranslation(); const collectionTrees = useCollectionTrees(); const [selectedPath, selectPath] = React.useState( @@ -51,9 +50,7 @@ function DocumentMove({ document }: Props) { const move = async () => { if (!selectedPath) { - showToast(t("Select a location to move"), { - type: "info", - }); + toast.message(t("Select a location to move")); return; } @@ -68,15 +65,11 @@ function DocumentMove({ document }: Props) { await document.move(collectionId); } - showToast(t("Document moved"), { - type: "success", - }); + toast.success(t("Document moved")); dialogs.closeAllModals(); } catch (err) { - showToast(t("Couldn’t move the document, try again?"), { - type: "error", - }); + toast.error(t("Couldn’t move the document, try again?")); } }; diff --git a/app/scenes/DocumentNew.tsx b/app/scenes/DocumentNew.tsx index 34cd1a985b..aadd2a1fe8 100644 --- a/app/scenes/DocumentNew.tsx +++ b/app/scenes/DocumentNew.tsx @@ -3,13 +3,13 @@ import * as React from "react"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useHistory, useLocation, useRouteMatch } from "react-router-dom"; +import { toast } from "sonner"; import CenteredContent from "~/components/CenteredContent"; import Flex from "~/components/Flex"; import PlaceholderDocument from "~/components/PlaceholderDocument"; import useCurrentUser from "~/hooks/useCurrentUser"; import useQuery from "~/hooks/useQuery"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import { documentEditPath, documentPath } from "~/utils/routeHelpers"; type Props = { @@ -25,7 +25,6 @@ function DocumentNew({ template }: Props) { const match = useRouteMatch<{ id?: string }>(); const { t } = useTranslation(); const { documents, collections } = useStores(); - const { showToast } = useToasts(); const id = match.params.id || query.get("collectionId"); useEffect(() => { @@ -56,9 +55,7 @@ function DocumentNew({ template }: Props) { location.state ); } catch (err) { - showToast(t("Couldn’t create the document, try again?"), { - type: "error", - }); + toast.error(t("Couldn’t create the document, try again?")); history.goBack(); } } diff --git a/app/scenes/DocumentPermanentDelete.tsx b/app/scenes/DocumentPermanentDelete.tsx index 52a1f5319f..81cefa0585 100644 --- a/app/scenes/DocumentPermanentDelete.tsx +++ b/app/scenes/DocumentPermanentDelete.tsx @@ -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 Document from "~/models/Document"; 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 = { document: Document; @@ -18,7 +18,6 @@ function DocumentPermanentDelete({ document, onSubmit }: Props) { const [isDeleting, setIsDeleting] = React.useState(false); const { t } = useTranslation(); const { documents } = useStores(); - const { showToast } = useToasts(); const history = useHistory(); const handleSubmit = React.useCallback( @@ -30,20 +29,16 @@ function DocumentPermanentDelete({ document, onSubmit }: Props) { await documents.delete(document, { permanent: true, }); - showToast(t("Document permanently deleted"), { - type: "success", - }); + toast.success(t("Document permanently deleted")); onSubmit(); history.push("/trash"); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { setIsDeleting(false); } }, - [document, onSubmit, showToast, t, history, documents] + [document, onSubmit, t, history, documents] ); return ( diff --git a/app/scenes/DocumentPublish.tsx b/app/scenes/DocumentPublish.tsx index 36a2083d49..9974696b2c 100644 --- a/app/scenes/DocumentPublish.tsx +++ b/app/scenes/DocumentPublish.tsx @@ -2,6 +2,7 @@ import flatten from "lodash/flatten"; import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import styled from "styled-components"; import { ellipsis } from "@shared/styles"; import { NavigationNode } from "@shared/types"; @@ -12,7 +13,6 @@ import Flex from "~/components/Flex"; import Text from "~/components/Text"; import useCollectionTrees from "~/hooks/useCollectionTrees"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import { flattenTree } from "~/utils/tree"; type Props = { @@ -22,7 +22,6 @@ type Props = { function DocumentPublish({ document }: Props) { const { dialogs } = useStores(); - const { showToast } = useToasts(); const { t } = useTranslation(); const collectionTrees = useCollectionTrees(); const [selectedPath, selectPath] = React.useState( @@ -35,9 +34,7 @@ function DocumentPublish({ document }: Props) { const publish = async () => { if (!selectedPath) { - showToast(t("Select a location to publish"), { - type: "info", - }); + toast.message(t("Select a location to publish")); return; } @@ -54,15 +51,11 @@ function DocumentPublish({ document }: Props) { document.collectionId = collectionId; await document.save(undefined, { publish: true }); - showToast(t("Document published"), { - type: "success", - }); + toast.success(t("Document published")); dialogs.closeAllModals(); } catch (err) { - showToast(t("Couldn’t publish the document, try again?"), { - type: "error", - }); + toast.error(t("Couldn’t publish the document, try again?")); } }; diff --git a/app/scenes/DocumentReparent.tsx b/app/scenes/DocumentReparent.tsx index 617bac8786..d88ecc2734 100644 --- a/app/scenes/DocumentReparent.tsx +++ b/app/scenes/DocumentReparent.tsx @@ -2,13 +2,13 @@ import { observer } from "mobx-react"; import { useState } from "react"; import * as React from "react"; import { Trans, useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { CollectionPermission, NavigationNode } from "@shared/types"; import Collection from "~/models/Collection"; 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 = { item: @@ -33,7 +33,6 @@ type Props = { function DocumentReparent({ collection, item, onSubmit, onCancel }: Props) { const [isSaving, setIsSaving] = useState(false); - const { showToast } = useToasts(); const { documents, collections } = useStores(); const { t } = useTranslation(); const prevCollection = collections.get(item.collectionId); @@ -50,19 +49,15 @@ function DocumentReparent({ collection, item, onSubmit, onCancel }: Props) { try { await documents.move(item.id, collection.id); - showToast(t("Document moved"), { - type: "info", - }); + toast.message(t("Document moved")); onSubmit(); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { setIsSaving(false); } }, - [documents, item.id, collection.id, showToast, t, onSubmit] + [documents, item.id, collection.id, t, onSubmit] ); return ( diff --git a/app/scenes/GroupDelete.tsx b/app/scenes/GroupDelete.tsx index 6a45006696..a3f464bc68 100644 --- a/app/scenes/GroupDelete.tsx +++ b/app/scenes/GroupDelete.tsx @@ -2,11 +2,11 @@ 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 Group from "~/models/Group"; import Button from "~/components/Button"; import Flex from "~/components/Flex"; import Text from "~/components/Text"; -import useToasts from "~/hooks/useToasts"; import { settingsPath } from "~/utils/routeHelpers"; type Props = { @@ -16,7 +16,6 @@ type Props = { function GroupDelete({ group, onSubmit }: Props) { const { t } = useTranslation(); - const { showToast } = useToasts(); const history = useHistory(); const [isDeleting, setIsDeleting] = React.useState(false); @@ -29,9 +28,7 @@ function GroupDelete({ group, onSubmit }: Props) { history.push(settingsPath("groups")); onSubmit(); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { setIsDeleting(false); } diff --git a/app/scenes/GroupEdit.tsx b/app/scenes/GroupEdit.tsx index f3907ab1e7..e78744b2d0 100644 --- a/app/scenes/GroupEdit.tsx +++ b/app/scenes/GroupEdit.tsx @@ -1,12 +1,12 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import Group from "~/models/Group"; import Button from "~/components/Button"; import Flex from "~/components/Flex"; import Input from "~/components/Input"; import Text from "~/components/Text"; -import useToasts from "~/hooks/useToasts"; type Props = { group: Group; @@ -14,7 +14,6 @@ type Props = { }; function GroupEdit({ group, onSubmit }: Props) { - const { showToast } = useToasts(); const { t } = useTranslation(); const [name, setName] = React.useState(group.name); const [isSaving, setIsSaving] = React.useState(false); @@ -29,14 +28,12 @@ function GroupEdit({ group, onSubmit }: Props) { }); onSubmit(); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { setIsSaving(false); } }, - [group, onSubmit, showToast, name] + [group, onSubmit, name] ); const handleNameChange = React.useCallback( diff --git a/app/scenes/GroupMembers/AddPeopleToGroup.tsx b/app/scenes/GroupMembers/AddPeopleToGroup.tsx index 1b673678a4..15d5583918 100644 --- a/app/scenes/GroupMembers/AddPeopleToGroup.tsx +++ b/app/scenes/GroupMembers/AddPeopleToGroup.tsx @@ -2,9 +2,12 @@ import debounce from "lodash/debounce"; import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import Group from "~/models/Group"; import User from "~/models/User"; import Invite from "~/scenes/Invite"; +import Avatar from "~/components/Avatar"; +import { AvatarSize } from "~/components/Avatar/Avatar"; import ButtonLink from "~/components/ButtonLink"; import Empty from "~/components/Empty"; import Flex from "~/components/Flex"; @@ -24,7 +27,7 @@ type Props = { function AddPeopleToGroup(props: Props) { const { group } = props; - const { users, auth, groupMemberships, toasts } = useStores(); + const { users, auth, groupMemberships } = useStores(); const { t } = useTranslation(); const [query, setQuery] = React.useState(""); @@ -53,18 +56,16 @@ function AddPeopleToGroup(props: Props) { userId: user.id, }); - toasts.showToast( + toast.success( t(`{{userName}} was added to the group`, { userName: user.name, }), { - type: "success", + icon: , } ); } catch (err) { - toasts.showToast(t("Could not add user"), { - type: "error", - }); + toast.error(t("Could not add user")); } }; diff --git a/app/scenes/GroupMembers/GroupMembers.tsx b/app/scenes/GroupMembers/GroupMembers.tsx index df62ad7249..37eb59be3a 100644 --- a/app/scenes/GroupMembers/GroupMembers.tsx +++ b/app/scenes/GroupMembers/GroupMembers.tsx @@ -2,6 +2,7 @@ import { observer } from "mobx-react"; import { PlusIcon } from "outline-icons"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import Group from "~/models/Group"; import User from "~/models/User"; import Button from "~/components/Button"; @@ -13,7 +14,6 @@ import Subheading from "~/components/Subheading"; import Text from "~/components/Text"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import AddPeopleToGroup from "./AddPeopleToGroup"; import GroupMemberListItem from "./components/GroupMemberListItem"; @@ -24,7 +24,6 @@ type Props = { function GroupMembers({ group }: Props) { const [addModalOpen, setAddModalOpen] = React.useState(false); const { users, groupMemberships } = useStores(); - const { showToast } = useToasts(); const { t } = useTranslation(); const can = usePolicy(group); @@ -38,18 +37,13 @@ function GroupMembers({ group }: Props) { groupId: group.id, userId: user.id, }); - showToast( + toast.success( t(`{{userName}} was removed from the group`, { userName: user.name, - }), - { - type: "success", - } + }) ); } catch (err) { - showToast(t("Could not remove user"), { - type: "error", - }); + toast.error(t("Could not remove user")); } }; diff --git a/app/scenes/GroupNew.tsx b/app/scenes/GroupNew.tsx index de9b044a39..c5338e257f 100644 --- a/app/scenes/GroupNew.tsx +++ b/app/scenes/GroupNew.tsx @@ -1,6 +1,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import Group from "~/models/Group"; import GroupMembers from "~/scenes/GroupMembers"; import Button from "~/components/Button"; @@ -9,7 +10,6 @@ import Input from "~/components/Input"; import Modal from "~/components/Modal"; import Text from "~/components/Text"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; type Props = { onSubmit: () => void; @@ -18,7 +18,6 @@ type Props = { function GroupNew({ onSubmit }: Props) { const { groups } = useStores(); const { t } = useTranslation(); - const { showToast } = useToasts(); const [name, setName] = React.useState(); const [isSaving, setIsSaving] = React.useState(false); const [group, setGroup] = React.useState(); @@ -38,9 +37,7 @@ function GroupNew({ onSubmit }: Props) { await group.save(); setGroup(group); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { setIsSaving(false); } diff --git a/app/scenes/Invite.tsx b/app/scenes/Invite.tsx index e56efa37a8..d078dae53f 100644 --- a/app/scenes/Invite.tsx +++ b/app/scenes/Invite.tsx @@ -3,6 +3,7 @@ import { LinkIcon, CloseIcon } from "outline-icons"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; import { Link } from "react-router-dom"; +import { toast } from "sonner"; import styled from "styled-components"; import { s } from "@shared/styles"; import { UserRole } from "@shared/types"; @@ -19,7 +20,6 @@ import useCurrentTeam from "~/hooks/useCurrentTeam"; import useCurrentUser from "~/hooks/useCurrentUser"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; type Props = { onSubmit: () => void; @@ -52,7 +52,6 @@ function Invite({ onSubmit }: Props) { }, ]); const { users } = useStores(); - const { showToast } = useToasts(); const user = useCurrentUser(); const team = useCurrentTeam(); const { t } = useTranslation(); @@ -69,23 +68,17 @@ function Invite({ onSubmit }: Props) { onSubmit(); if (data.sent.length > 0) { - showToast(t("We sent out your invites!"), { - type: "success", - }); + toast.success(t("We sent out your invites!")); } else { - showToast(t("Those email addresses are already invited"), { - type: "success", - }); + toast.message(t("Those email addresses are already invited")); } } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { setIsSaving(false); } }, - [onSubmit, showToast, invites, t, users] + [onSubmit, invites, t, users] ); const handleChange = React.useCallback((ev, index) => { @@ -98,13 +91,10 @@ function Invite({ onSubmit }: Props) { const handleAdd = React.useCallback(() => { if (invites.length >= UserValidation.maxInvitesPerRequest) { - showToast( + toast.message( t("Sorry, you can only send {{MAX_INVITES}} invites at a time", { MAX_INVITES: UserValidation.maxInvitesPerRequest, - }), - { - type: "warning", - } + }) ); } @@ -117,7 +107,7 @@ function Invite({ onSubmit }: Props) { }); return newInvites; }); - }, [showToast, invites, t]); + }, [invites, t]); const handleRemove = React.useCallback( (ev: React.SyntheticEvent, index: number) => { @@ -133,10 +123,8 @@ function Invite({ onSubmit }: Props) { const handleCopy = React.useCallback(() => { setLinkCopied(true); - showToast(t("Share link copied"), { - type: "success", - }); - }, [showToast, t]); + toast.success(t("Share link copied")); + }, [t]); const handleRoleChange = React.useCallback( (role: UserRole, index: number) => { diff --git a/app/scenes/Settings/Details.tsx b/app/scenes/Settings/Details.tsx index 2c2f25d247..79a6a8faeb 100644 --- a/app/scenes/Settings/Details.tsx +++ b/app/scenes/Settings/Details.tsx @@ -5,6 +5,7 @@ import { TeamIcon } from "outline-icons"; import { useRef, useState } from "react"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import { ThemeProvider, useTheme } from "styled-components"; import { buildDarkTheme, buildLightTheme } from "@shared/styles/theme"; import { CustomTheme, TeamPreference } from "@shared/types"; @@ -21,7 +22,6 @@ import Text from "~/components/Text"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import isCloudHosted from "~/utils/isCloudHosted"; import TeamDelete from "../TeamDelete"; import ImageInput from "./components/ImageInput"; @@ -29,7 +29,6 @@ import SettingRow from "./components/SettingRow"; function Details() { const { auth, dialogs, ui } = useStores(); - const { showToast } = useToasts(); const { t } = useTranslation(); const team = useCurrentTeam(); const theme = useTheme(); @@ -76,13 +75,9 @@ function Details() { customTheme, }, }); - showToast(t("Settings saved"), { - type: "success", - }); + toast.success(t("Settings saved")); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }, [ @@ -93,7 +88,6 @@ function Details() { team.preferences, publicBranding, customTheme, - showToast, t, ] ); @@ -116,16 +110,14 @@ function Details() { await auth.updateTeam({ avatarUrl, }); - showToast(t("Logo updated"), { - type: "success", - }); + toast.success(t("Logo updated")); }; const handleAvatarError = React.useCallback( (error: string | null | undefined) => { - showToast(error || t("Unable to upload new logo")); + toast.error(error || t("Unable to upload new logo")); }, - [showToast, t] + [t] ); const showDeleteWorkspace = () => { diff --git a/app/scenes/Settings/Features.tsx b/app/scenes/Settings/Features.tsx index db00fc722f..9c0f1a84f1 100644 --- a/app/scenes/Settings/Features.tsx +++ b/app/scenes/Settings/Features.tsx @@ -2,6 +2,7 @@ import { observer } from "mobx-react"; import { BeakerIcon } from "outline-icons"; import * as React from "react"; import { Trans, useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { TeamPreference } from "@shared/types"; import Heading from "~/components/Heading"; import Scene from "~/components/Scene"; @@ -9,14 +10,12 @@ import Switch from "~/components/Switch"; import Text from "~/components/Text"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import SettingRow from "./components/SettingRow"; function Features() { const { auth } = useStores(); const team = useCurrentTeam(); const { t } = useTranslation(); - const { showToast } = useToasts(); const handlePreferenceChange = (inverted = false) => @@ -27,9 +26,7 @@ function Features() { }; await auth.updateTeam({ preferences }); - showToast(t("Settings saved"), { - type: "success", - }); + toast.success(t("Settings saved")); }; return ( diff --git a/app/scenes/Settings/GoogleAnalytics.tsx b/app/scenes/Settings/GoogleAnalytics.tsx index 22e6a3c318..56c07a1cd1 100644 --- a/app/scenes/Settings/GoogleAnalytics.tsx +++ b/app/scenes/Settings/GoogleAnalytics.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useForm } from "react-hook-form"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import { IntegrationType, IntegrationService } from "@shared/types"; import Integration from "~/models/Integration"; import Button from "~/components/Button"; @@ -12,7 +13,6 @@ import Input from "~/components/Input"; import Scene from "~/components/Scene"; import Text from "~/components/Text"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import SettingRow from "./components/SettingRow"; type FormData = { @@ -22,7 +22,6 @@ type FormData = { function GoogleAnalytics() { const { integrations } = useStores(); const { t } = useTranslation(); - const { showToast } = useToasts(); const integration = find(integrations.orderedData, { type: IntegrationType.Analytics, @@ -67,16 +66,12 @@ function GoogleAnalytics() { await integration?.delete(); } - showToast(t("Settings saved"), { - type: "success", - }); + toast.success(t("Settings saved")); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }, - [integrations, integration, t, showToast] + [integrations, integration, t] ); return ( diff --git a/app/scenes/Settings/Notifications.tsx b/app/scenes/Settings/Notifications.tsx index 1c1d5bb5bb..6946251634 100644 --- a/app/scenes/Settings/Notifications.tsx +++ b/app/scenes/Settings/Notifications.tsx @@ -13,6 +13,7 @@ import { } from "outline-icons"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import { NotificationEventType } from "@shared/types"; import Flex from "~/components/Flex"; import Heading from "~/components/Heading"; @@ -23,12 +24,10 @@ import Switch from "~/components/Switch"; import Text from "~/components/Text"; import env from "~/env"; import useCurrentUser from "~/hooks/useCurrentUser"; -import useToasts from "~/hooks/useToasts"; import isCloudHosted from "~/utils/isCloudHosted"; import SettingRow from "./components/SettingRow"; function Notifications() { - const { showToast } = useToasts(); const user = useCurrentUser(); const { t } = useTranslation(); @@ -106,9 +105,7 @@ function Notifications() { ]; const showSuccessMessage = debounce(() => { - showToast(t("Notifications saved"), { - type: "success", - }); + toast.success(t("Notifications saved")); }, 500); const handleChange = React.useCallback( diff --git a/app/scenes/Settings/Preferences.tsx b/app/scenes/Settings/Preferences.tsx index 6d4248539d..ce9875f950 100644 --- a/app/scenes/Settings/Preferences.tsx +++ b/app/scenes/Settings/Preferences.tsx @@ -2,6 +2,7 @@ import { observer } from "mobx-react"; import { SettingsIcon } from "outline-icons"; import * as React from "react"; import { Trans, useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { languageOptions } from "@shared/i18n"; import { TeamPreference, UserPreference } from "@shared/types"; import Button from "~/components/Button"; @@ -13,13 +14,11 @@ import Text from "~/components/Text"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import UserDelete from "../UserDelete"; import SettingRow from "./components/SettingRow"; function Preferences() { const { t } = useTranslation(); - const { showToast } = useToasts(); const { dialogs, auth } = useStores(); const user = useCurrentUser(); const team = useCurrentTeam(); @@ -33,16 +32,12 @@ function Preferences() { }; await auth.updateUser({ preferences }); - showToast(t("Preferences saved"), { - type: "success", - }); + toast.success(t("Preferences saved")); }; const handleLanguageChange = async (language: string) => { await auth.updateUser({ language }); - showToast(t("Preferences saved"), { - type: "success", - }); + toast.success(t("Preferences saved")); }; const showDeleteAccount = () => { diff --git a/app/scenes/Settings/Profile.tsx b/app/scenes/Settings/Profile.tsx index 999b6c827d..a88db05dbc 100644 --- a/app/scenes/Settings/Profile.tsx +++ b/app/scenes/Settings/Profile.tsx @@ -2,6 +2,7 @@ import { observer } from "mobx-react"; import { ProfileIcon } from "outline-icons"; import * as React from "react"; import { Trans, useTranslation } from "react-i18next"; +import { toast } from "sonner"; import Button from "~/components/Button"; import Heading from "~/components/Heading"; import Input from "~/components/Input"; @@ -9,7 +10,6 @@ import Scene from "~/components/Scene"; import Text from "~/components/Text"; import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import ImageInput from "./components/ImageInput"; import SettingRow from "./components/SettingRow"; @@ -18,7 +18,6 @@ const Profile = () => { const user = useCurrentUser(); const form = React.useRef(null); const [name, setName] = React.useState(user.name || ""); - const { showToast } = useToasts(); const { t } = useTranslation(); const handleSubmit = async (ev: React.SyntheticEvent) => { @@ -28,13 +27,9 @@ const Profile = () => { await auth.updateUser({ name, }); - showToast(t("Profile saved"), { - type: "success", - }); + toast.success(t("Profile saved")); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }; @@ -46,15 +41,11 @@ const Profile = () => { await auth.updateUser({ avatarUrl, }); - showToast(t("Profile picture updated"), { - type: "success", - }); + toast.success(t("Profile picture updated")); }; const handleAvatarError = (error: string | null | undefined) => { - showToast(error || t("Unable to upload new profile picture"), { - type: "error", - }); + toast.error(error || t("Unable to upload new profile picture")); }; const isValid = form.current?.checkValidity(); diff --git a/app/scenes/Settings/Security.tsx b/app/scenes/Settings/Security.tsx index e952156be4..2cbbb683d0 100644 --- a/app/scenes/Settings/Security.tsx +++ b/app/scenes/Settings/Security.tsx @@ -4,6 +4,7 @@ import { CheckboxIcon, EmailIcon, PadlockIcon } from "outline-icons"; import { useState } from "react"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import { useTheme } from "styled-components"; import { TeamPreference } from "@shared/types"; import ConfirmationDialog from "~/components/ConfirmationDialog"; @@ -18,7 +19,6 @@ import env from "~/env"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import useRequest from "~/hooks/useRequest"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import isCloudHosted from "~/utils/isCloudHosted"; import DomainManagement from "./components/DomainManagement"; import SettingRow from "./components/SettingRow"; @@ -27,7 +27,6 @@ function Security() { const { auth, authenticationProviders, dialogs } = useStores(); const team = useCurrentTeam(); const { t } = useTranslation(); - const { showToast } = useToasts(); const theme = useTheme(); const [data, setData] = useState({ sharing: team.sharing, @@ -53,11 +52,9 @@ function Security() { const showSuccessMessage = React.useMemo( () => debounce(() => { - showToast(t("Settings saved"), { - type: "success", - }); + toast.success(t("Settings saved")); }, 250), - [showToast, t] + [t] ); const saveData = React.useCallback( @@ -67,12 +64,10 @@ function Security() { await auth.updateTeam(newData); showSuccessMessage(); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }, - [auth, showSuccessMessage, showToast] + [auth, showSuccessMessage] ); const handleChange = React.useCallback( diff --git a/app/scenes/Settings/SelfHosted.tsx b/app/scenes/Settings/SelfHosted.tsx index ce56859daf..bc836d218d 100644 --- a/app/scenes/Settings/SelfHosted.tsx +++ b/app/scenes/Settings/SelfHosted.tsx @@ -4,6 +4,7 @@ import { BuildingBlocksIcon } from "outline-icons"; import * as React from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { IntegrationService, IntegrationType } from "@shared/types"; import Integration from "~/models/Integration"; import Button from "~/components/Button"; @@ -11,7 +12,6 @@ import Heading from "~/components/Heading"; import Input from "~/components/Input"; import Scene from "~/components/Scene"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import SettingRow from "./components/SettingRow"; type FormData = { @@ -22,7 +22,6 @@ type FormData = { function SelfHosted() { const { integrations } = useStores(); const { t } = useTranslation(); - const { showToast } = useToasts(); const integrationDiagrams = find(integrations.orderedData, { type: IntegrationType.Embed, @@ -89,16 +88,12 @@ function SelfHosted() { await integrationGrist?.delete(); } - showToast(t("Settings saved"), { - type: "success", - }); + toast.success(t("Settings saved")); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }, - [integrations, integrationDiagrams, integrationGrist, t, showToast] + [integrations, integrationDiagrams, integrationGrist, t] ); return ( diff --git a/app/scenes/Settings/components/ApiKeyListItem.tsx b/app/scenes/Settings/components/ApiKeyListItem.tsx index de3daf642e..606ef3a987 100644 --- a/app/scenes/Settings/components/ApiKeyListItem.tsx +++ b/app/scenes/Settings/components/ApiKeyListItem.tsx @@ -1,12 +1,12 @@ import { CopyIcon } from "outline-icons"; import * as React from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import ApiKey from "~/models/ApiKey"; import Button from "~/components/Button"; import CopyToClipboard from "~/components/CopyToClipboard"; import Flex from "~/components/Flex"; import ListItem from "~/components/List/Item"; -import useToasts from "~/hooks/useToasts"; import ApiKeyMenu from "~/menus/ApiKeyMenu"; type Props = { @@ -15,7 +15,6 @@ type Props = { const ApiKeyListItem = ({ apiKey }: Props) => { const { t } = useTranslation(); - const { showToast } = useToasts(); const [linkCopied, setLinkCopied] = React.useState(false); React.useEffect(() => { @@ -28,10 +27,8 @@ const ApiKeyListItem = ({ apiKey }: Props) => { const handleCopy = React.useCallback(() => { setLinkCopied(true); - showToast(t("API token copied to clipboard"), { - type: "success", - }); - }, [showToast, t]); + toast.message(t("API token copied to clipboard")); + }, [t]); return ( { const newDomains = allowedDomains.filter((_, i) => index !== i); diff --git a/app/scenes/Settings/components/DropToImport.tsx b/app/scenes/Settings/components/DropToImport.tsx index 8123c383b2..733f5f45cd 100644 --- a/app/scenes/Settings/components/DropToImport.tsx +++ b/app/scenes/Settings/components/DropToImport.tsx @@ -3,13 +3,13 @@ import { NewDocumentIcon } from "outline-icons"; import * as React from "react"; import Dropzone from "react-dropzone"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import styled from "styled-components"; import { s } from "@shared/styles"; import { AttachmentPreset } from "@shared/types"; import Flex from "~/components/Flex"; import LoadingIndicator from "~/components/LoadingIndicator"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import { uploadFile } from "~/utils/files"; type Props = { @@ -23,15 +23,12 @@ type Props = { function DropToImport({ disabled, onSubmit, children, format }: Props) { const { t } = useTranslation(); const { collections } = useStores(); - const { showToast } = useToasts(); const [isImporting, setImporting] = React.useState(false); const handleFiles = React.useCallback( async (files) => { if (files.length > 1) { - showToast(t("Please choose a single file to import"), { - type: "error", - }); + toast.error(t("Please choose a single file to import")); return; } const file = files[0]; @@ -45,27 +42,21 @@ function DropToImport({ disabled, onSubmit, children, format }: Props) { }); await collections.import(attachment.id, format); onSubmit(); - showToast( - t("Your import is being processed, you can safely leave this page"), - { - type: "success", - timeout: 6000, - } + toast.success( + t("Your import is being processed, you can safely leave this page") ); } catch (err) { - showToast(err.message); + toast.error(err.message); } finally { setImporting(false); } }, - [t, onSubmit, collections, format, showToast] + [t, onSubmit, collections, format] ); const handleRejection = React.useCallback(() => { - showToast(t("File not supported – please upload a valid ZIP file"), { - type: "error", - }); - }, [t, showToast]); + toast.error(t("File not supported – please upload a valid ZIP file")); + }, [t]); if (disabled) { return children; diff --git a/app/scenes/Settings/components/FileOperationListItem.tsx b/app/scenes/Settings/components/FileOperationListItem.tsx index 65ab6a0b5c..87729bab43 100644 --- a/app/scenes/Settings/components/FileOperationListItem.tsx +++ b/app/scenes/Settings/components/FileOperationListItem.tsx @@ -2,6 +2,7 @@ import { observer } from "mobx-react"; import { ArchiveIcon, DoneIcon, WarningIcon } from "outline-icons"; import * as React from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { useTheme } from "styled-components"; import { FileOperationFormat, @@ -16,7 +17,6 @@ import Spinner from "~/components/Spinner"; import Time from "~/components/Time"; import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import FileOperationMenu from "~/menus/FileOperationMenu"; type Props = { @@ -28,7 +28,6 @@ const FileOperationListItem = ({ fileOperation }: Props) => { const user = useCurrentUser(); const theme = useTheme(); const { dialogs, fileOperations } = useStores(); - const { showToast } = useToasts(); const stateMapping = { [FileOperationState.Creating]: t("Processing"), @@ -65,16 +64,14 @@ const FileOperationListItem = ({ fileOperation }: Props) => { await fileOperations.delete(fileOperation); if (fileOperation.type === FileOperationType.Import) { - showToast(t("Import deleted")); + toast.success(t("Import deleted")); } else { - showToast(t("Export deleted")); + toast.success(t("Export deleted")); } } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } - }, [fileOperation, fileOperations, showToast, t]); + }, [fileOperation, fileOperations, t]); const handleConfirmDelete = React.useCallback(async () => { dialogs.openModal({ diff --git a/app/scenes/TeamDelete.tsx b/app/scenes/TeamDelete.tsx index 4ac3dc516e..152528c1bf 100644 --- a/app/scenes/TeamDelete.tsx +++ b/app/scenes/TeamDelete.tsx @@ -2,6 +2,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useForm } from "react-hook-form"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import Button from "~/components/Button"; import Flex from "~/components/Flex"; import Input from "~/components/Input"; @@ -9,7 +10,6 @@ import Text from "~/components/Text"; import env from "~/env"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; type FormData = { code: string; @@ -22,7 +22,6 @@ type Props = { function TeamDelete({ onSubmit }: Props) { const [isWaitingCode, setWaitingCode] = React.useState(false); const { auth } = useStores(); - const { showToast } = useToasts(); const team = useCurrentTeam(); const { t } = useTranslation(); const { @@ -39,12 +38,10 @@ function TeamDelete({ onSubmit }: Props) { await auth.requestDeleteTeam(); setWaitingCode(true); } catch (error) { - showToast(error.message, { - type: "error", - }); + toast.error(error.message); } }, - [auth, showToast] + [auth] ); const handleSubmit = React.useCallback( @@ -54,12 +51,10 @@ function TeamDelete({ onSubmit }: Props) { await auth.logout(); onSubmit(); } catch (error) { - showToast(error.message, { - type: "error", - }); + toast.error(error.message); } }, - [auth, onSubmit, showToast] + [auth, onSubmit] ); const inputProps = register("code", { diff --git a/app/scenes/TeamNew.tsx b/app/scenes/TeamNew.tsx index 36d155aaf3..81702e3aac 100644 --- a/app/scenes/TeamNew.tsx +++ b/app/scenes/TeamNew.tsx @@ -1,6 +1,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import User from "~/models/User"; import Button from "~/components/Button"; import Flex from "~/components/Flex"; @@ -8,7 +9,6 @@ import Input from "~/components/Input"; import Notice from "~/components/Notice"; import Text from "~/components/Text"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; type Props = { user: User; @@ -17,7 +17,6 @@ type Props = { function TeamNew({ user }: Props) { const { auth } = useStores(); const { t } = useTranslation(); - const { showToast } = useToasts(); const [name, setName] = React.useState(""); const [isSaving, setIsSaving] = React.useState(false); @@ -32,9 +31,7 @@ function TeamNew({ user }: Props) { }); } } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } finally { setIsSaving(false); } diff --git a/app/scenes/UserDelete.tsx b/app/scenes/UserDelete.tsx index e4ee0093d1..34e40ce912 100644 --- a/app/scenes/UserDelete.tsx +++ b/app/scenes/UserDelete.tsx @@ -2,13 +2,13 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useForm } from "react-hook-form"; import { useTranslation, Trans } from "react-i18next"; +import { toast } from "sonner"; import Button from "~/components/Button"; import Flex from "~/components/Flex"; import Input from "~/components/Input"; import Text from "~/components/Text"; import env from "~/env"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; type FormData = { code: string; @@ -17,7 +17,6 @@ type FormData = { function UserDelete() { const [isWaitingCode, setWaitingCode] = React.useState(false); const { auth } = useStores(); - const { showToast } = useToasts(); const { t } = useTranslation(); const { register, @@ -32,13 +31,11 @@ function UserDelete() { try { await auth.requestDeleteUser(); setWaitingCode(true); - } catch (error) { - showToast(error.message, { - type: "error", - }); + } catch (err) { + toast.error(err.message); } }, - [auth, showToast] + [auth] ); const handleSubmit = React.useCallback( @@ -46,13 +43,11 @@ function UserDelete() { try { await auth.deleteUser(data); await auth.logout(); - } catch (error) { - showToast(error.message, { - type: "error", - }); + } catch (err) { + toast.error(err.message); } }, - [auth, showToast] + [auth] ); const inputProps = register("code", { diff --git a/app/stores/RootStore.ts b/app/stores/RootStore.ts index 15bb17535e..27fa44ace0 100644 --- a/app/stores/RootStore.ts +++ b/app/stores/RootStore.ts @@ -21,7 +21,6 @@ import SearchesStore from "./SearchesStore"; import SharesStore from "./SharesStore"; import StarsStore from "./StarsStore"; import SubscriptionsStore from "./SubscriptionsStore"; -import ToastsStore from "./ToastsStore"; import UiStore from "./UiStore"; import UsersStore from "./UsersStore"; import ViewsStore from "./ViewsStore"; @@ -53,7 +52,6 @@ export default class RootStore { subscriptions: SubscriptionsStore; users: UsersStore; views: ViewsStore; - toasts: ToastsStore; fileOperations: FileOperationsStore; webhookSubscriptions: WebhookSubscriptionsStore; @@ -85,7 +83,6 @@ export default class RootStore { this.users = new UsersStore(this); this.views = new ViewsStore(this); this.fileOperations = new FileOperationsStore(this); - this.toasts = new ToastsStore(); this.webhookSubscriptions = new WebhookSubscriptionsStore(this); } diff --git a/app/stores/ToastsStore.test.ts b/app/stores/ToastsStore.test.ts deleted file mode 100644 index acf81056d4..0000000000 --- a/app/stores/ToastsStore.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import stores from "."; - -describe("ToastsStore", () => { - const store = stores.toasts; - - test("#add should add messages", () => { - expect(store.orderedData.length).toBe(0); - - store.showToast("first error"); - store.showToast("second error"); - 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.orderedData.length).toBe(2); - id && store.hideToast(id); - - expect(store.orderedData.length).toBe(1); - expect(store.orderedData[0].message).toBe("second error"); - }); -}); diff --git a/app/stores/ToastsStore.ts b/app/stores/ToastsStore.ts deleted file mode 100644 index ce3d16a4aa..0000000000 --- a/app/stores/ToastsStore.ts +++ /dev/null @@ -1,55 +0,0 @@ -import orderBy from "lodash/orderBy"; -import { observable, action, computed } from "mobx"; -import { v4 as uuidv4 } from "uuid"; -import { 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?.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/types.ts b/app/types.ts index f1500384ea..108fdc3158 100644 --- a/app/types.ts +++ b/app/types.ts @@ -121,19 +121,6 @@ export type LocationWithState = Location & { state: Record; }; -export type Toast = { - id: string; - createdAt: string; - message: string; - type: "warning" | "error" | "info" | "success" | "loading"; - timeout?: number; - reoccurring?: number; - action?: { - text: string; - onClick: React.MouseEventHandler; - }; -}; - export type FetchOptions = { prefetch?: boolean; revisionId?: string; @@ -177,15 +164,6 @@ export type SearchResult = { document: Document; }; -export type ToastOptions = { - type: "warning" | "error" | "info" | "success" | "loading"; - timeout?: number; - action?: { - text: string; - onClick: React.MouseEventHandler; - }; -}; - export type WebsocketEntityDeletedEvent = { modelId: string; }; diff --git a/package.json b/package.json index 7406f56f06..8f936c50d9 100644 --- a/package.json +++ b/package.json @@ -211,6 +211,7 @@ "socket.io": "^4.7.2", "socket.io-client": "^4.6.1", "socket.io-redis": "^6.1.1", + "sonner": "^1.0.3", "stoppable": "^1.1.0", "string-replace-to-array": "^2.1.0", "styled-components": "^5.3.11", diff --git a/plugins/slack/client/components/SlackListItem.tsx b/plugins/slack/client/components/SlackListItem.tsx index 096fcc7c67..660bab9de2 100644 --- a/plugins/slack/client/components/SlackListItem.tsx +++ b/plugins/slack/client/components/SlackListItem.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { Trans, useTranslation } from "react-i18next"; import { usePopoverState, PopoverDisclosure } from "reakit/Popover"; +import { toast } from "sonner"; import styled from "styled-components"; import { s } from "@shared/styles"; import { IntegrationType } from "@shared/types"; @@ -16,7 +17,6 @@ import ListItem from "~/components/List/Item"; import Popover from "~/components/Popover"; import Switch from "~/components/Switch"; import Text from "~/components/Text"; -import useToasts from "~/hooks/useToasts"; type Props = { integration: Integration; @@ -25,7 +25,6 @@ type Props = { function SlackListItem({ integration, collection }: Props) { const { t } = useTranslation(); - const { showToast } = useToasts(); const handleChange = async (ev: React.ChangeEvent) => { if (ev.target.checked) { @@ -38,9 +37,7 @@ function SlackListItem({ integration, collection }: Props) { await integration.save(); - showToast(t("Settings saved"), { - type: "success", - }); + toast.success(t("Settings saved")); }; const mapping = { diff --git a/plugins/webhooks/client/components/WebhookSubscriptionEdit.tsx b/plugins/webhooks/client/components/WebhookSubscriptionEdit.tsx index 5c5401a46e..96602aefdc 100644 --- a/plugins/webhooks/client/components/WebhookSubscriptionEdit.tsx +++ b/plugins/webhooks/client/components/WebhookSubscriptionEdit.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import WebhookSubscription from "~/models/WebhookSubscription"; -import useToasts from "~/hooks/useToasts"; import WebhookSubscriptionForm from "./WebhookSubscriptionForm"; type Props = { @@ -16,7 +16,6 @@ interface FormData { } function WebhookSubscriptionEdit({ onSubmit, webhookSubscription }: Props) { - const { showToast } = useToasts(); const { t } = useTranslation(); const handleSubmit = React.useCallback( @@ -31,19 +30,13 @@ function WebhookSubscriptionEdit({ onSubmit, webhookSubscription }: Props) { await webhookSubscription.save(toSend); - showToast( - t("Webhook updated", { - type: "success", - }) - ); + toast.success(t("Webhook updated")); onSubmit(); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }, - [t, showToast, onSubmit, webhookSubscription] + [t, onSubmit, webhookSubscription] ); return ( diff --git a/plugins/webhooks/client/components/WebhookSubscriptionNew.tsx b/plugins/webhooks/client/components/WebhookSubscriptionNew.tsx index 2e1167c887..4acdc67a1a 100644 --- a/plugins/webhooks/client/components/WebhookSubscriptionNew.tsx +++ b/plugins/webhooks/client/components/WebhookSubscriptionNew.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import useStores from "~/hooks/useStores"; -import useToasts from "~/hooks/useToasts"; import WebhookSubscriptionForm from "./WebhookSubscriptionForm"; type Props = { @@ -16,7 +16,6 @@ interface FormData { function WebhookSubscriptionNew({ onSubmit }: Props) { const { webhookSubscriptions } = useStores(); - const { showToast } = useToasts(); const { t } = useTranslation(); const handleSubmit = React.useCallback( @@ -30,19 +29,13 @@ function WebhookSubscriptionNew({ onSubmit }: Props) { }; await webhookSubscriptions.create(toSend); - showToast( - t("Webhook created", { - type: "success", - }) - ); + toast.success(t("Webhook created")); onSubmit(); } catch (err) { - showToast(err.message, { - type: "error", - }); + toast.error(err.message); } }, - [t, showToast, onSubmit, webhookSubscriptions] + [t, onSubmit, webhookSubscriptions] ); return ; diff --git a/shared/editor/commands/createAndInsertLink.ts b/shared/editor/commands/createAndInsertLink.ts index e8f69f2d5b..9fa2db873e 100644 --- a/shared/editor/commands/createAndInsertLink.ts +++ b/shared/editor/commands/createAndInsertLink.ts @@ -1,5 +1,6 @@ import { Node } from "prosemirror-model"; import { EditorView } from "prosemirror-view"; +import { toast } from "sonner"; function findPlaceholderLink(doc: Node, href: string) { let result: { pos: number; node: Node } | undefined; @@ -38,11 +39,10 @@ const createAndInsertLink = async function ( options: { dictionary: any; onCreateLink: (title: string) => Promise; - onShowToast: (message: string) => void; } ) { const { dispatch, state } = view; - const { onCreateLink, onShowToast } = options; + const { onCreateLink } = options; try { const url = await onCreateLink(title); @@ -79,7 +79,7 @@ const createAndInsertLink = async function ( ) ); - onShowToast(options.dictionary.createLinkError); + toast.error(options.dictionary.createLinkError); } }; diff --git a/shared/editor/commands/insertFiles.ts b/shared/editor/commands/insertFiles.ts index 39e71dbc5d..a1941d0666 100644 --- a/shared/editor/commands/insertFiles.ts +++ b/shared/editor/commands/insertFiles.ts @@ -1,5 +1,6 @@ import * as Sentry from "@sentry/react"; import { EditorView } from "prosemirror-view"; +import { toast } from "sonner"; import { v4 as uuidv4 } from "uuid"; import FileHelper from "../lib/FileHelper"; import uploadPlaceholderPlugin, { @@ -19,8 +20,6 @@ export type Options = { onFileUploadStart?: () => void; /** Callback fired when the user completes a file upload */ onFileUploadStop?: () => void; - /** Callback fired when a toast needs to be displayed */ - onShowToast: (message: string) => void; /** Attributes to overwrite */ attrs?: { /** Width to use when inserting image */ @@ -40,13 +39,8 @@ const insertFiles = function ( files: File[], options: Options ): void { - const { - dictionary, - uploadFile, - onFileUploadStart, - onFileUploadStop, - onShowToast, - } = options; + const { dictionary, uploadFile, onFileUploadStart, onFileUploadStop } = + options; // okay, we have some dropped files and a handler – lets stop this // event going any further up the stack @@ -172,7 +166,7 @@ const insertFiles = function ( }) ); - onShowToast(error.message || dictionary.fileUploadError); + toast.error(error.message || dictionary.fileUploadError); }) .finally(() => { complete++; diff --git a/shared/editor/marks/Link.tsx b/shared/editor/marks/Link.tsx index dbcbaf2ef3..ecfdfdd0a3 100644 --- a/shared/editor/marks/Link.tsx +++ b/shared/editor/marks/Link.tsx @@ -13,6 +13,7 @@ import { Command, EditorState, Plugin } from "prosemirror-state"; import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import * as React from "react"; import ReactDOM from "react-dom"; +import { toast } from "sonner"; import { isExternalUrl, sanitizeUrl } from "../../utils/urls"; import findLinkNodes from "../queries/findLinkNodes"; import getMarkRange from "../queries/getMarkRange"; @@ -138,9 +139,7 @@ export default class Link extends Mark { event ); } catch (err) { - this.editor.props.onShowToast( - this.options.dictionary.openLinkError - ); + toast.error(this.options.dictionary.openLinkError); } return true; } @@ -177,9 +176,7 @@ export default class Link extends Mark { ); } } catch (err) { - this.editor.props.onShowToast( - this.options.dictionary.openLinkError - ); + toast.error(this.options.dictionary.openLinkError); } }); return cloned; @@ -246,9 +243,7 @@ export default class Link extends Mark { this.options.onClickLink(sanitizeUrl(href), event); } } catch (err) { - this.editor.props.onShowToast( - this.options.dictionary.openLinkError - ); + toast.error(this.options.dictionary.openLinkError); } return true; diff --git a/shared/editor/nodes/CodeFence.ts b/shared/editor/nodes/CodeFence.ts index 409cf25c34..dbd86ab715 100644 --- a/shared/editor/nodes/CodeFence.ts +++ b/shared/editor/nodes/CodeFence.ts @@ -56,6 +56,7 @@ import visualbasic from "refractor/lang/visual-basic"; import yaml from "refractor/lang/yaml"; import zig from "refractor/lang/zig"; +import { toast } from "sonner"; import { Primitive } from "utility-types"; import { Dictionary } from "~/hooks/useDictionary"; import { UserPreferences } from "../../types"; @@ -130,7 +131,6 @@ export default class CodeFence extends Node { constructor(options: { dictionary: Dictionary; userPreferences?: UserPreferences | null; - onShowToast: (message: string) => void; }) { super(options); } @@ -197,7 +197,7 @@ export default class CodeFence extends Node { } copy(codeBlock.node.textContent); - this.options.onShowToast(this.options.dictionary.codeCopied); + toast.message(this.options.dictionary.codeCopied); return true; }, }; diff --git a/shared/editor/nodes/Heading.ts b/shared/editor/nodes/Heading.ts index 66e832ca8f..40843c8beb 100644 --- a/shared/editor/nodes/Heading.ts +++ b/shared/editor/nodes/Heading.ts @@ -8,6 +8,7 @@ import { } from "prosemirror-model"; import { Command, Plugin, Selection } from "prosemirror-state"; import { Decoration, DecorationSet } from "prosemirror-view"; +import { toast } from "sonner"; import { Primitive } from "utility-types"; import Storage from "../../utils/Storage"; import backspaceToParagraph from "../commands/backspaceToParagraph"; @@ -188,7 +189,7 @@ export default class Heading extends Node { .replace("/edit", ""); copy(normalizedUrl + hash); - this.options.onShowToast(this.options.dictionary.linkCopied); + toast.message(this.options.dictionary.linkCopied); }; keys({ type, schema }: { type: NodeType; schema: Schema }) { diff --git a/shared/editor/nodes/SimpleImage.tsx b/shared/editor/nodes/SimpleImage.tsx index dbf1028425..d5243eccbf 100644 --- a/shared/editor/nodes/SimpleImage.tsx +++ b/shared/editor/nodes/SimpleImage.tsx @@ -125,7 +125,7 @@ export default class SimpleImage extends Node { } const { view } = this.editor; const { node } = state.selection; - const { uploadFile, onFileUploadStart, onFileUploadStop, onShowToast } = + const { uploadFile, onFileUploadStart, onFileUploadStop } = this.editor.props; if (!uploadFile) { @@ -146,7 +146,6 @@ export default class SimpleImage extends Node { uploadFile, onFileUploadStart, onFileUploadStop, - onShowToast, dictionary: this.options.dictionary, replaceExisting: true, attrs: { diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index b5b0f7f7c3..a95707afb3 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -132,6 +132,7 @@ "Submenu": "Submenu", "Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app", "Default collection": "Default collection", + "Install now": "Install now", "Deleted Collection": "Deleted Collection", "Unpin": "Unpin", "Search collections & documents": "Search collections & documents", @@ -191,9 +192,9 @@ "{{userName}} published": "{{userName}} published", "{{userName}} unpublished": "{{userName}} unpublished", "{{userName}} moved": "{{userName}} moved", - "Your file will be available in {{ location }} soon": "Your file will be available in {{ location }} soon", - "Go to exports": "Go to exports", "Export started": "Export started", + "Your file will be available in {{ location }} soon": "Your file will be available in {{ location }} soon", + "View": "View", "A ZIP file containing the images, and documents in the Markdown format.": "A ZIP file containing the images, and documents in the Markdown format.", "A ZIP file containing the images, and documents as HTML files.": "A ZIP file containing the images, and documents as HTML files.", "Structured data that can be used to transfer data to another compatible {{ appName }} instance.": "Structured data that can be used to transfer data to another compatible {{ appName }} instance.", diff --git a/shared/styles/theme.ts b/shared/styles/theme.ts index f76e9c6a27..40a9bbd623 100644 --- a/shared/styles/theme.ts +++ b/shared/styles/theme.ts @@ -152,8 +152,8 @@ export const buildLightTheme = (input: Partial): DefaultTheme => { buttonNeutralBorder: darken(0.15, colors.white), tooltipBackground: colors.almostBlack, tooltipText: colors.white, - toastBackground: colors.almostBlack, - toastText: colors.white, + toastBackground: colors.white, + toastText: colors.almostBlack, quote: colors.slateLight, codeBackground: colors.smoke, codeBorder: colors.smokeDark, @@ -215,8 +215,8 @@ export const buildDarkTheme = (input: Partial): DefaultTheme => { buttonNeutralBorder: colors.slateDark, tooltipBackground: colors.white, tooltipText: colors.lightBlack, - toastBackground: colors.white, - toastText: colors.lightBlack, + toastBackground: colors.veryDarkBlue, + toastText: colors.almostWhite, quote: colors.almostWhite, code: colors.almostWhite, codeBackground: colors.black75, diff --git a/yarn.lock b/yarn.lock index a3f048ba9a..8e3c5d3166 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,14 +40,7 @@ "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3" chokidar "^3.4.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" - integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== - dependencies: - "@babel/highlight" "^7.22.5" - -"@babel/code-frame@^7.22.13": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.22.5": version "7.22.13" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== @@ -81,17 +74,7 @@ json5 "^2.2.2" semver "^6.3.0" -"@babel/generator@^7.22.5", "@babel/generator@^7.7.2": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.5.tgz#1e7bf768688acfb05cf30b2369ef855e82d984f7" - integrity sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA== - dependencies: - "@babel/types" "^7.22.5" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/generator@^7.23.0": +"@babel/generator@^7.22.5", "@babel/generator@^7.23.0", "@babel/generator@^7.7.2": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== @@ -175,12 +158,7 @@ resolve "^1.14.2" semver "^6.1.2" -"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" - integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== - -"@babel/helper-environment-visitor@^7.22.20": +"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.22.5": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== @@ -192,15 +170,7 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" - integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== - dependencies: - "@babel/template" "^7.22.5" - "@babel/types" "^7.22.5" - -"@babel/helper-function-name@^7.23.0": +"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0", "@babel/helper-function-name@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== @@ -291,14 +261,7 @@ dependencies: "@babel/types" "^7.20.0" -"@babel/helper-split-export-declaration@^7.18.6", "@babel/helper-split-export-declaration@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz#88cf11050edb95ed08d596f7a044462189127a08" - integrity sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-split-export-declaration@^7.22.6": +"@babel/helper-split-export-declaration@^7.18.6", "@babel/helper-split-export-declaration@^7.22.5", "@babel/helper-split-export-declaration@^7.22.6": version "7.22.6" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== @@ -339,7 +302,7 @@ "@babel/traverse" "^7.22.5" "@babel/types" "^7.22.5" -"@babel/highlight@^7.22.13": +"@babel/highlight@^7.22.13", "@babel/highlight@^7.22.5": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== @@ -348,21 +311,7 @@ chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/highlight@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" - integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== - dependencies: - "@babel/helper-validator-identifier" "^7.22.5" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.22.5", "@babel/parser@^7.7.0": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.5.tgz#721fd042f3ce1896238cf1b341c77eb7dee7dbea" - integrity sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q== - -"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.22.15", "@babel/parser@^7.22.5", "@babel/parser@^7.23.0", "@babel/parser@^7.7.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== @@ -1110,16 +1059,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.22.5", "@babel/template@^7.3.3": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" - integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== - dependencies: - "@babel/code-frame" "^7.22.5" - "@babel/parser" "^7.22.5" - "@babel/types" "^7.22.5" - -"@babel/template@^7.22.15": +"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.3.3": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== @@ -1144,16 +1084,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": - version "7.22.19" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.19.tgz#7425343253556916e440e662bb221a93ddb75684" - integrity sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg== - dependencies: - "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.19" - to-fast-properties "^2.0.0" - -"@babel/types@^7.23.0": +"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== @@ -7795,15 +7726,7 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= -import-fresh@^3.2.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.2.tgz#fc129c160c5d68235507f4331a6baad186bdbc3e" - integrity sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-fresh@^3.3.0: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -12189,6 +12112,11 @@ socks@^2.3.3: ip "^2.0.0" smart-buffer "^4.2.0" +sonner@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.0.3.tgz#3d5c08f1773c28e98e51dba527350d4ac1c912a2" + integrity sha512-hBoA2zKuYW3lUnpx4K0vAn8j77YuYiwvP9sLQfieNS2pd5FkT20sMyPTDJnl9S+5T27ZJbwQRPiujwvDBwhZQg== + sort-keys@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-5.0.0.tgz#5d775f8ae93ecc29bc7312bbf3acac4e36e3c446"