import * as React from 'react'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; import { Alert, Flex } from '@strapi/design-system'; import { Link } from '@strapi/design-system/v2'; import { useCallbackRef } from '../hooks/useCallbackRef'; /** * TODO: realistically a lot of this logic is isolated to the `core/admin` package. * However, we want to expose the `useNotification` hook to the plugins. * * Therefore, in V5 we should move this logic back to the `core/admin` package & export * the hook from that package and re-export here. For now, let's keep it all together * because it's easier to diagnose and we're not using a million refs because we don't * understand what's going on. */ /** * @preserve * @typedef {Object} NotificationLink * @property {string | import('react-intl').MessageDescriptor} label * @property {string | undefined} target * @property {string} url */ /** * @preserve * @typedef {Object} NotificationConfig * @property {boolean | undefined} blockTransition * @property {NotificationLink} link * @property {string | import('react-intl').MessageDescriptor | undefined} message * @property {() => void | undefined} onClose * @property {number | undefined} timeout * @property {string | import('react-intl').MessageDescriptor | undefined} title * @property {"info" | "warning" | "softWarning" | "success" | undefined} type */ /* ------------------------------------------------------------------------------------------------- * Context * -----------------------------------------------------------------------------------------------*/ /** * @preserve * @typedef {Object} NotificationsContextValue * @property {(config: NotificationConfig) => void} toggleNotification – Toggles a notification, wrapped in `useCallback` for a stable identity. */ /** * @type {React.Context} */ const NotificationsContext = React.createContext(); /* ------------------------------------------------------------------------------------------------- * Provider * -----------------------------------------------------------------------------------------------*/ const NotificationsProvider = ({ children }) => { const notificationIdRef = React.useRef(0); const [notifications, setNotifications] = React.useState([]); const toggleNotification = React.useCallback( ({ type, message, link, timeout, blockTransition, onClose, title }) => { setNotifications((s) => [ ...s, { id: notificationIdRef.current++, type, message, link, timeout, blockTransition, onClose, title, }, ]); }, [] ); const clearNotification = React.useCallback((id) => { setNotifications((s) => s.filter((n) => n.id !== id)); }, []); const value = React.useMemo(() => ({ toggleNotification }), [toggleNotification]); return ( {notifications.map((notification) => { return ( ); })} {children} ); }; NotificationsProvider.propTypes = { children: PropTypes.node.isRequired, }; const Notification = ({ id, clearNotification, message, link, type, onClose, timeout, blockTransition, title, }) => { const { formatMessage } = useIntl(); /** * Chances are `onClose` won't be classed as stabilised, * so we use `useCallbackRef` to avoid make it stable. */ const onCloseCallback = useCallbackRef(onClose); const handleClose = React.useCallback(() => { onCloseCallback(); clearNotification(id); }, [clearNotification, id, onCloseCallback]); // eslint-disable-next-line consistent-return React.useEffect(() => { if (!blockTransition) { const timeoutReference = setTimeout(() => { handleClose(); }, timeout); return () => { clearTimeout(timeoutReference); }; } }, [blockTransition, handleClose, timeout]); let variant; let alertTitle; if (type === 'info') { variant = 'default'; alertTitle = formatMessage({ id: 'notification.default.title', defaultMessage: 'Information:', }); } else if (type === 'warning') { // TODO: type should be renamed to danger in the future, but it might introduce changes if done now variant = 'danger'; alertTitle = formatMessage({ id: 'notification.warning.title', defaultMessage: 'Warning:', }); } else if (type === 'softWarning') { // TODO: type should be renamed to just warning in the future variant = 'warning'; alertTitle = formatMessage({ id: 'notification.warning.title', defaultMessage: 'Warning:', }); } else { variant = 'success'; alertTitle = formatMessage({ id: 'notification.success.title', defaultMessage: 'Success:', }); } if (title) { alertTitle = typeof title === 'string' ? title : formatMessage( { id: title?.id || title, defaultMessage: title?.defaultMessage || title?.id || title, }, title?.values ); } return ( {formatMessage({ id: link.label?.id || link.label, defaultMessage: link.label?.defaultMessage || link.label?.id || link.label, })} ) : undefined } onClose={handleClose} closeLabel={formatMessage({ id: 'global.close', defaultMessage: 'Close', })} title={alertTitle} variant={variant} > {formatMessage( { id: message?.id || message, defaultMessage: message?.defaultMessage || message?.id || message, }, message?.values )} ); }; Notification.defaultProps = { blockTransition: false, link: undefined, onClose: undefined, message: { id: 'notification.success.saved', defaultMessage: 'Saved', }, timeout: 2500, title: undefined, type: 'success', }; Notification.propTypes = { id: PropTypes.number.isRequired, clearNotification: PropTypes.func.isRequired, message: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({ id: PropTypes.string.isRequired, defaultMessage: PropTypes.string, values: PropTypes.object, }), ]), link: PropTypes.shape({ target: PropTypes.string, url: PropTypes.string.isRequired, label: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({ id: PropTypes.string.isRequired, defaultMessage: PropTypes.string, values: PropTypes.object, }), ]).isRequired, }), type: PropTypes.string, onClose: PropTypes.func, timeout: PropTypes.number, blockTransition: PropTypes.bool, title: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({ id: PropTypes.string.isRequired, defaultMessage: PropTypes.string, values: PropTypes.object, }), ]), }; /* ------------------------------------------------------------------------------------------------- * Hook * -----------------------------------------------------------------------------------------------*/ /** * @preserve * @description Returns an object to interact with the notification * system. The callbacks are wrapped in `useCallback` for a stable * identity. * * @returns {NotificationsContextValue} * * @example * ```tsx * import { useNotification } from '@strapi/helper-plugin'; * * const MyComponent = () => { * const { toggleNotification } = useNotification(); * * return ; */ const useNotification = () => React.useContext(NotificationsContext).toggleNotification; export { NotificationsProvider, useNotification, NotificationsContext };