From c7138fbba6402f50a0c7de929d5c1d18a95e1b71 Mon Sep 17 00:00:00 2001 From: Esen Date: Wed, 10 May 2023 21:31:04 +0600 Subject: [PATCH 1/7] feat(notifications): prevent duplicates --- src/components/notifier/Notification.tsx | 10 ---- src/components/notifier/notifier.tsx | 72 +++++++++++++----------- src/components/notifier/stories.tsx | 58 ++++++++++++++++++- 3 files changed, 95 insertions(+), 45 deletions(-) diff --git a/src/components/notifier/Notification.tsx b/src/components/notifier/Notification.tsx index 67b837e34..615bcece7 100644 --- a/src/components/notifier/Notification.tsx +++ b/src/components/notifier/Notification.tsx @@ -55,13 +55,3 @@ export const Notification = ({ ); }; - -export interface INotification { - /** Title of the alert. */ - title: string | ReactNode; - /** Content under the title. */ - content: string | ReactNode; - /** Duration in milliseconds. E.g. 5000 by default */ - duration?: number; - isError?: boolean; -} diff --git a/src/components/notifier/notifier.tsx b/src/components/notifier/notifier.tsx index cfdf88b64..32d2b607b 100644 --- a/src/components/notifier/notifier.tsx +++ b/src/components/notifier/notifier.tsx @@ -4,10 +4,16 @@ import { standaloneToast } from '../../theme-chakra/ChakraThemeProvider'; import { Notification } from './Notification'; export interface INotification { + /** Title of the alert. */ title: string | ReactNode; + /** Content under the title. */ content: string | ReactNode; + /** Duration in milliseconds. E.g. 5000 by default */ duration?: number; + /** Status e.g. warning, error, success */ status?: AlertStatus; + /** A unique ID that blocks other notifications with the same ID */ + uniqueId?: ToastId; } export const useNotifier = () => { @@ -16,35 +22,35 @@ export const useNotifier = () => { const notify = useCallback( (status: AlertStatus) => (notification: INotification) => { const render = ({ onClose, id }: { onClose(): void; id: ToastId }) => { + const duration = notification.duration ?? 5000; + + const dontLetToastDisappear = () => + toast.update(id, { duration: 1e6, render }); + + const letToastDisappear = () => toast.update(id, { duration, render }); + return ( hoverHandler(id)} - onMouseLeave={() => unhoverHandler(id)} + onMouseEnter={dontLetToastDisappear} + onMouseLeave={letToastDisappear} /> ); }; - const hoverHandler = (id: ToastId) => { - toast.update(id, { duration: 1e6, render }); - }; - - const unhoverHandler = (id: ToastId) => { - toast.update(id, { + if (!(notification.uniqueId && toast.isActive(notification.uniqueId))) { + return toast({ + status, duration: notification.duration ?? 5000, + isClosable: true, render, + id: notification.uniqueId, }); - }; - - toast({ - status, - duration: notification.duration ?? 5000, - isClosable: true, - render, - }); + } + return null; }, [toast], ); @@ -60,9 +66,9 @@ export const useNotifier = () => { }; export const createNotifier = () => { - const notify = (status: AlertStatus) => (notification: INotification) => { - const render = ({ onClose, id }: { onClose(): void; id: ToastId }) => { - return ( + const notify = (status: AlertStatus) => { + return (notification: INotification) => { + const render = ({ onClose, id }: { onClose(): void; id: ToastId }) => ( { onMouseLeave={() => unhoverHandler(id)} /> ); - }; - const hoverHandler = (id: ToastId) => { - standaloneToast.update(id, { duration: 1e6, render }); - }; + const hoverHandler = (id: ToastId) => { + standaloneToast.update(id, { duration: 1e6, render }); + }; - const unhoverHandler = (id: ToastId) => { - standaloneToast.update(id, { + const unhoverHandler = (id: ToastId) => { + standaloneToast.update(id, { + duration: notification.duration ?? 5000, + render, + }); + }; + + return standaloneToast({ + status, duration: notification.duration ?? 5000, + isClosable: true, + position: 'top-right', render, }); }; - - return standaloneToast({ - status, - duration: notification.duration ?? 5000, - isClosable: true, - position: 'top-right', - render, - }); }; return { diff --git a/src/components/notifier/stories.tsx b/src/components/notifier/stories.tsx index 6b6de116d..ce7e848e5 100644 --- a/src/components/notifier/stories.tsx +++ b/src/components/notifier/stories.tsx @@ -8,8 +8,8 @@ import { Box } from '../box'; import { Button } from '../button'; import { Flex } from '../flex'; import Value from '../typography/value'; -import { INotification } from './Notification'; -import { createNotifier, useNotifier } from './notifier'; +import { INotification, createNotifier, useNotifier } from './notifier'; +import { Label, Labeling } from '../..'; const meta: Meta = { title: 'Notifier', @@ -160,6 +160,60 @@ export const Success: Story = { }, }; +export const PreventDuplicateNotifications: Story = { + render: (args) => { + const notify = useNotifier(); + + const showRegular = () => { + notify.info({ + title: `Regular: ${args.title}`, + content: args.content, + duration: args.duration, + }); + }; + + const showPrevented = () => { + notify.success({ + title: `Prevents others: ${args.title}`, + content: args.content, + duration: args.duration, + uniqueId: 'just one id', + }); + }; + + return ( + + + If you pass an ID to a notifier, it is going to prevent other + notifications with the same ID from being shown. + + + + + + + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const portal = within(document.querySelector('.chakra-portal')!); + + userEvent.click(canvas.getByText('Regular')); + userEvent.click(canvas.getByText('Regular')); + userEvent.click(canvas.getByText('Regular')); + expect( + await portal.findAllByText('Regular: Something happened'), + ).toHaveLength(3); + + userEvent.click(canvas.getByText('With ID given')); + userEvent.click(canvas.getByText('With ID given')); + userEvent.click(canvas.getByText('With ID given')); + expect( + await portal.findAllByText('Prevents others: Something happened'), + ).toHaveLength(1); + }, +}; + export const Standalone: Story = { parameters: { docs: { From 26094e3ff92efec778c0e0956e94eceed93404ad Mon Sep 17 00:00:00 2001 From: Esen Date: Wed, 10 May 2023 21:41:15 +0600 Subject: [PATCH 2/7] make it work for standalone too --- src/components/notifier/notifier.tsx | 61 ++++++++++++++++------------ 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/src/components/notifier/notifier.tsx b/src/components/notifier/notifier.tsx index 32d2b607b..4a8d92e27 100644 --- a/src/components/notifier/notifier.tsx +++ b/src/components/notifier/notifier.tsx @@ -21,9 +21,9 @@ export const useNotifier = () => { const notify = useCallback( (status: AlertStatus) => (notification: INotification) => { - const render = ({ onClose, id }: { onClose(): void; id: ToastId }) => { - const duration = notification.duration ?? 5000; + const duration = notification.duration ?? 5000; + const render = ({ onClose, id }: { onClose(): void; id: ToastId }) => { const dontLetToastDisappear = () => toast.update(id, { duration: 1e6, render }); @@ -44,7 +44,7 @@ export const useNotifier = () => { if (!(notification.uniqueId && toast.isActive(notification.uniqueId))) { return toast({ status, - duration: notification.duration ?? 5000, + duration, isClosable: true, render, id: notification.uniqueId, @@ -68,35 +68,42 @@ export const useNotifier = () => { export const createNotifier = () => { const notify = (status: AlertStatus) => { return (notification: INotification) => { - const render = ({ onClose, id }: { onClose(): void; id: ToastId }) => ( - hoverHandler(id)} - onMouseLeave={() => unhoverHandler(id)} - /> - ); + const duration = notification.duration ?? 5000; + + const render = ({ onClose, id }: { onClose(): void; id: ToastId }) => { + const dontLetToastDisappear = () => + standaloneToast.update(id, { duration: 1e6, render }); + const letToastDisappear = () => + standaloneToast.update(id, { duration, render }); - const hoverHandler = (id: ToastId) => { - standaloneToast.update(id, { duration: 1e6, render }); + return ( + + ); }; - const unhoverHandler = (id: ToastId) => { - standaloneToast.update(id, { - duration: notification.duration ?? 5000, + if ( + !( + notification.uniqueId && + standaloneToast.isActive(notification.uniqueId) + ) + ) { + return standaloneToast({ + position: 'top-right', + status, + duration, + isClosable: true, render, + id: notification.uniqueId, }); - }; - - return standaloneToast({ - status, - duration: notification.duration ?? 5000, - isClosable: true, - position: 'top-right', - render, - }); + } + return null; }; }; From 283c597257d1ce77a8dd97500322e2b3367ec713 Mon Sep 17 00:00:00 2001 From: Esen Date: Wed, 10 May 2023 21:42:39 +0600 Subject: [PATCH 3/7] remove unused imports --- src/components/notifier/stories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/notifier/stories.tsx b/src/components/notifier/stories.tsx index ce7e848e5..e4f7fb9a3 100644 --- a/src/components/notifier/stories.tsx +++ b/src/components/notifier/stories.tsx @@ -9,7 +9,6 @@ import { Button } from '../button'; import { Flex } from '../flex'; import Value from '../typography/value'; import { INotification, createNotifier, useNotifier } from './notifier'; -import { Label, Labeling } from '../..'; const meta: Meta = { title: 'Notifier', From a164c87ce5a833b6b81b3155843d1693b8930d03 Mon Sep 17 00:00:00 2001 From: Ehsan Heydari Date: Thu, 11 May 2023 15:20:29 +0200 Subject: [PATCH 4/7] Abstract notifier renderer --- src/components/notifier/notifier.tsx | 80 +++++++++++----------------- 1 file changed, 31 insertions(+), 49 deletions(-) diff --git a/src/components/notifier/notifier.tsx b/src/components/notifier/notifier.tsx index 4a8d92e27..1f4777731 100644 --- a/src/components/notifier/notifier.tsx +++ b/src/components/notifier/notifier.tsx @@ -1,5 +1,11 @@ -import { AlertStatus, ToastId, useToast } from '@chakra-ui/react'; +import { + AlertStatus, + CreateToastFnReturn, + ToastId, + useToast, +} from '@chakra-ui/react'; import React, { ReactNode, useCallback } from 'react'; +import * as R from 'ramda'; import { standaloneToast } from '../../theme-chakra/ChakraThemeProvider'; import { Notification } from './Notification'; @@ -16,18 +22,25 @@ export interface INotification { uniqueId?: ToastId; } -export const useNotifier = () => { - const toast = useToast(); - - const notify = useCallback( - (status: AlertStatus) => (notification: INotification) => { +const buildNotifier = R.curryN( + 2, + (toast: CreateToastFnReturn, status: AlertStatus) => + (notification: INotification) => { const duration = notification.duration ?? 5000; - const render = ({ onClose, id }: { onClose(): void; id: ToastId }) => { + const duration = notification.duration ?? 5000; + const dontLetToastDisappear = () => - toast.update(id, { duration: 1e6, render }); + toast.update(id, { + duration: 1e6, + render, + }); - const letToastDisappear = () => toast.update(id, { duration, render }); + const letToastDisappear = () => + toast.update(id, { + duration, + render, + }); return ( { if (!(notification.uniqueId && toast.isActive(notification.uniqueId))) { return toast({ + position: 'top-right', status, duration, isClosable: true, @@ -52,6 +66,13 @@ export const useNotifier = () => { } return null; }, +); + +export const useNotifier = () => { + const toast = useToast(); + + const notify = useCallback( + (status: AlertStatus) => buildNotifier(toast, status), [toast], ); @@ -66,46 +87,7 @@ export const useNotifier = () => { }; export const createNotifier = () => { - const notify = (status: AlertStatus) => { - return (notification: INotification) => { - const duration = notification.duration ?? 5000; - - const render = ({ onClose, id }: { onClose(): void; id: ToastId }) => { - const dontLetToastDisappear = () => - standaloneToast.update(id, { duration: 1e6, render }); - const letToastDisappear = () => - standaloneToast.update(id, { duration, render }); - - return ( - - ); - }; - - if ( - !( - notification.uniqueId && - standaloneToast.isActive(notification.uniqueId) - ) - ) { - return standaloneToast({ - position: 'top-right', - status, - duration, - isClosable: true, - render, - id: notification.uniqueId, - }); - } - return null; - }; - }; + const notify = buildNotifier(standaloneToast); return { success: notify('success'), From 6c8154f50713f24f5a69a651b6c071e266fbe241 Mon Sep 17 00:00:00 2001 From: Ehsan Heydari Date: Thu, 11 May 2023 15:43:46 +0200 Subject: [PATCH 5/7] chore: more clear --- src/components/notifier/notifier.tsx | 78 ++++++++++++++-------------- 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/src/components/notifier/notifier.tsx b/src/components/notifier/notifier.tsx index 1f4777731..7efe84daf 100644 --- a/src/components/notifier/notifier.tsx +++ b/src/components/notifier/notifier.tsx @@ -5,7 +5,6 @@ import { useToast, } from '@chakra-ui/react'; import React, { ReactNode, useCallback } from 'react'; -import * as R from 'ramda'; import { standaloneToast } from '../../theme-chakra/ChakraThemeProvider'; import { Notification } from './Notification'; @@ -22,51 +21,49 @@ export interface INotification { uniqueId?: ToastId; } -const buildNotifier = R.curryN( - 2, +const buildNotifier = (toast: CreateToastFnReturn, status: AlertStatus) => - (notification: INotification) => { + (notification: INotification) => { + const duration = notification.duration ?? 5000; + const render = ({ onClose, id }: { onClose(): void; id: ToastId }) => { const duration = notification.duration ?? 5000; - const render = ({ onClose, id }: { onClose(): void; id: ToastId }) => { - const duration = notification.duration ?? 5000; - const dontLetToastDisappear = () => - toast.update(id, { - duration: 1e6, - render, - }); - - const letToastDisappear = () => - toast.update(id, { - duration, - render, - }); - - return ( - - ); - }; + const dontLetToastDisappear = () => + toast.update(id, { + duration: 1e6, + render, + }); - if (!(notification.uniqueId && toast.isActive(notification.uniqueId))) { - return toast({ - position: 'top-right', - status, + const letToastDisappear = () => + toast.update(id, { duration, - isClosable: true, render, - id: notification.uniqueId, }); - } - return null; - }, -); + + return ( + + ); + }; + + if (!(notification.uniqueId && toast.isActive(notification.uniqueId))) { + return toast({ + position: 'top-right', + status, + duration, + isClosable: true, + render, + id: notification.uniqueId, + }); + } + return null; + }; export const useNotifier = () => { const toast = useToast(); @@ -87,7 +84,8 @@ export const useNotifier = () => { }; export const createNotifier = () => { - const notify = buildNotifier(standaloneToast); + const notify = (status: AlertStatus) => + buildNotifier(standaloneToast, status); return { success: notify('success'), From 14a9c2eb44ff32a79e9551d025a31c007fa82bbb Mon Sep 17 00:00:00 2001 From: Ehsan Heydari Date: Thu, 11 May 2023 15:47:49 +0200 Subject: [PATCH 6/7] Reuse methods creation --- src/components/notifier/notifier.tsx | 29 ++++++++++++---------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/components/notifier/notifier.tsx b/src/components/notifier/notifier.tsx index 7efe84daf..49f160c98 100644 --- a/src/components/notifier/notifier.tsx +++ b/src/components/notifier/notifier.tsx @@ -4,7 +4,7 @@ import { ToastId, useToast, } from '@chakra-ui/react'; -import React, { ReactNode, useCallback } from 'react'; +import React, { ReactNode, useCallback, useMemo } from 'react'; import { standaloneToast } from '../../theme-chakra/ChakraThemeProvider'; import { Notification } from './Notification'; @@ -65,6 +65,15 @@ const buildNotifier = return null; }; +const createMethods = (toast: CreateToastFnReturn, notify: any) => ({ + success: notify('success'), + error: notify('error'), + info: notify('info'), + warning: notify('warning'), + closeAll: toast.closeAll, + close: toast.close, +}); + export const useNotifier = () => { const toast = useToast(); @@ -73,26 +82,12 @@ export const useNotifier = () => { [toast], ); - return { - success: notify('success'), - error: notify('error'), - info: notify('info'), - warning: notify('warning'), - closeAll: toast.closeAll, - close: toast.close, - }; + return useMemo(() => createMethods(toast, notify), [notify, toast]); }; export const createNotifier = () => { const notify = (status: AlertStatus) => buildNotifier(standaloneToast, status); - return { - success: notify('success'), - error: notify('error'), - info: notify('info'), - warning: notify('warning'), - closeAll: standaloneToast.closeAll, - close: standaloneToast.close, - }; + return createMethods(standaloneToast, notify); }; From eeed9182f06c52de6baf3ae40a5faa4ad6cc7aa1 Mon Sep 17 00:00:00 2001 From: Ehsan Heydari Date: Thu, 11 May 2023 16:05:03 +0200 Subject: [PATCH 7/7] Update Type + rename --- src/components/notifier/notifier.tsx | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/components/notifier/notifier.tsx b/src/components/notifier/notifier.tsx index 49f160c98..5d82d6afb 100644 --- a/src/components/notifier/notifier.tsx +++ b/src/components/notifier/notifier.tsx @@ -65,11 +65,16 @@ const buildNotifier = return null; }; -const createMethods = (toast: CreateToastFnReturn, notify: any) => ({ - success: notify('success'), - error: notify('error'), - info: notify('info'), - warning: notify('warning'), +type Notifier = ReturnType; + +const createMethods = ( + toast: CreateToastFnReturn, + notifyWithStatus: (status: AlertStatus) => Notifier, +) => ({ + success: notifyWithStatus('success'), + error: notifyWithStatus('error'), + info: notifyWithStatus('info'), + warning: notifyWithStatus('warning'), closeAll: toast.closeAll, close: toast.close, }); @@ -77,17 +82,20 @@ const createMethods = (toast: CreateToastFnReturn, notify: any) => ({ export const useNotifier = () => { const toast = useToast(); - const notify = useCallback( + const notifyWithStatus = useCallback( (status: AlertStatus) => buildNotifier(toast, status), [toast], ); - return useMemo(() => createMethods(toast, notify), [notify, toast]); + return useMemo( + () => createMethods(toast, notifyWithStatus), + [notifyWithStatus, toast], + ); }; export const createNotifier = () => { - const notify = (status: AlertStatus) => + const notifyWithStatus = (status: AlertStatus) => buildNotifier(standaloneToast, status); - return createMethods(standaloneToast, notify); + return createMethods(standaloneToast, notifyWithStatus); };