2023-05-30 10:03:47 +02:00

306 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<NotificationsContextValue>}
*/
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 (
<NotificationsContext.Provider value={value}>
<Flex
left="50%"
marginLeft="-250px"
position="fixed"
direction="column"
alignItems="stretch"
gap={2}
top={`${46 / 16}rem`}
width={`${500 / 16}rem`}
zIndex={10}
>
{notifications.map((notification) => {
return (
<Notification
key={notification.id}
{...notification}
clearNotification={clearNotification}
/>
);
})}
</Flex>
{children}
</NotificationsContext.Provider>
);
};
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 (
<Alert
action={
link ? (
<Link href={link.url} isExternal>
{formatMessage({
id: link.label?.id || link.label,
defaultMessage: link.label?.defaultMessage || link.label?.id || link.label,
})}
</Link>
) : 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
)}
</Alert>
);
};
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 <button onClick={() => toggleNotification({ message: 'Hello world!' })}>Click me</button>;
*/
const useNotification = () => React.useContext(NotificationsContext).toggleNotification;
export { NotificationsProvider, useNotification, NotificationsContext };