feat(ui, dashboard): Toast rework (#7076)
**What** - Re-works how toasts work in Medusa UI. API is now built on top of `sonner` instead of `@radix-ui/react-toast`. This is a breaking change, and we will need to update the documentation once this has been merged and released (cc: @shahednasser). - Adds an example of usage in the products list table in the new admin dashboard. As part of the coming weeks cleanup we will add toasts everywhere that they are currently missing. CLOSES CORE-1977
This commit is contained in:
@@ -1 +0,0 @@
|
||||
export * from "./use-toast"
|
||||
@@ -1,85 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { Toaster } from "@/components/toaster"
|
||||
import { useToast } from "./use-toast"
|
||||
|
||||
const Demo = () => {
|
||||
const { toast } = useToast()
|
||||
|
||||
const handleUndo = async () => {
|
||||
// Wait 3 seconds then resolve with true
|
||||
const confirmed = await new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(true)
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
return confirmed
|
||||
}
|
||||
|
||||
const handleSchedule = () => {
|
||||
const onAction = async (fn: (update: any) => void) => {
|
||||
fn({
|
||||
title: "Processing",
|
||||
description: "Removing post from queue.",
|
||||
variant: "loading",
|
||||
disableDismiss: true,
|
||||
action: undefined,
|
||||
})
|
||||
|
||||
const confirmed = await handleUndo().then((confirmed) => {
|
||||
if (confirmed) {
|
||||
fn({
|
||||
title: "Success",
|
||||
description: "Your post was successfully removed from the queue.",
|
||||
variant: "success",
|
||||
disableDismiss: false,
|
||||
})
|
||||
} else {
|
||||
fn({
|
||||
title: "Error",
|
||||
description:
|
||||
"Something went wrong, and your post was not removed from the queue. Try again.",
|
||||
variant: "error",
|
||||
disableDismiss: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const t = toast({
|
||||
title: "Scheduled",
|
||||
description: "Your post has been scheduled for 12:00 PM",
|
||||
variant: "success",
|
||||
action: {
|
||||
label: "Undo",
|
||||
onClick: () => onAction(t.update),
|
||||
altText:
|
||||
"Alternatively, you can undo this action by navigating to the scheduled posts page, and clicking the unschedule button.",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-w-screen flex min-h-screen flex-col items-center justify-center">
|
||||
<Button onClick={handleSchedule}>Schedule</Button>
|
||||
<Toaster />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const meta: Meta<typeof Demo> = {
|
||||
title: "Hooks/useToast",
|
||||
component: Demo,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Demo>
|
||||
|
||||
export const Default: Story = {}
|
||||
@@ -1,188 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import type { ToastActionElement, ToastProps } from "@/components/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_VALUE
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { toast, useToast }
|
||||
Reference in New Issue
Block a user