import {cloneElement, Component, Fragment, isValidElement} from 'react'; import {ModalRenderProps, openModal} from 'sentry/actionCreators/modal'; import Button, {ButtonProps} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import {t} from 'sentry/locale'; export type ConfirmMessageRenderProps = { /** * Closes the modal, if `bypass` is true, will call `onConfirm` callback */ close: (e: React.MouseEvent) => void; /** * Confirms the modal */ confirm: () => void; /** * Set the disabled state of the confirm button */ disableConfirmButton: (disable: boolean) => void; /** * When the modal is confirmed the function registered will be called. * * Useful if your rendered message contains some functionality that should be * triggered upon the modal being confirmed. * * This should be called in the components componentDidMount. */ setConfirmCallback: (cb: () => void) => void; }; export type ConfirmButtonsRenderProps = { /** * Applications can call this function to manually close the modal. */ closeModal: () => void; /** * The default onClick behavior, including closing the modal and triggering the * onConfirm / onCancel callbacks. */ defaultOnClick: () => void; }; type ChildrenRenderProps = { open: () => void; }; export type OpenConfirmOptions = { /** * If true, will skip the confirmation modal and call `onConfirm` callback */ bypass?: boolean; /** * Text to show in the cancel button */ cancelText?: React.ReactNode; /** * Text to show in the confirmation button */ confirmText?: React.ReactNode; /** * Disables the confirm button. * * XXX: Once the modal has been opened mutating this property will _not_ * propagate into the modal. * * If you need the confirm buttons disabled state to be reactively * controlled, consider using the renderMessage prop, which receives a * `disableConfirmButton` function that you may use to control the state of it. */ disableConfirmButton?: boolean; /** * Header of modal */ header?: React.ReactNode; /** * Message to display to user when asking for confirmation */ message?: React.ReactNode; /** * User cancels the modal */ onCancel?: () => void; /** * Callback when user confirms */ onConfirm?: () => void; /** * Callback function when user is in the confirming state called when the * confirm modal is opened */ onConfirming?: () => void; /** * Button priority */ priority?: ButtonProps['priority']; /** * Custom function to render the cancel button */ renderCancelButton?: (props: ConfirmButtonsRenderProps) => React.ReactNode; /** * Custom function to render the confirm button */ renderConfirmButton?: (props: ConfirmButtonsRenderProps) => React.ReactNode; /** * Used to render a message instead of using the static `message` prop. */ renderMessage?: (renderProps: ConfirmMessageRenderProps) => React.ReactNode; }; interface Props extends OpenConfirmOptions { /** * Render props to control rendering of the modal in its entirety */ children?: | ((renderProps: ChildrenRenderProps) => React.ReactNode) | React.ReactElement<{disabled: boolean; onClick: (e: React.MouseEvent) => void}>; /** * Passed to `children` render function */ disabled?: boolean; /** * Stop event propagation when opening the confirm modal */ stopPropagation?: boolean; } /** * Opens a confirmation modal when called. The procedural version of the * `Confirm` component */ export const openConfirmModal = ({ bypass, onConfirming, priority = 'primary', cancelText = t('Cancel'), confirmText = t('Confirm'), disableConfirmButton = false, ...rest }: OpenConfirmOptions) => { if (bypass) { rest.onConfirm?.(); return; } const modalProps = { ...rest, priority, confirmText, cancelText, disableConfirmButton, }; onConfirming?.(); openModal(renderProps => ); }; /** * The confirm component is somewhat special in that you can wrap any * onClick-able element with this to trigger a interstitial confirmation modal. * * This is the declarative alternative to using openConfirmModal */ function Confirm({ disabled, children, stopPropagation = false, ...openConfirmOptions }: Props) { const triggerModal = (e?: React.MouseEvent) => { if (stopPropagation) { e?.stopPropagation(); } if (disabled) { return; } openConfirmModal(openConfirmOptions); }; if (typeof children === 'function') { return children({open: triggerModal}); } if (!isValidElement(children)) { return null; } // TODO(ts): Understand why the return type of `cloneElement` is strange return cloneElement(children, {disabled, onClick: triggerModal}) as any; } type ModalProps = ModalRenderProps & Pick< Props, | 'priority' | 'renderMessage' | 'renderConfirmButton' | 'renderCancelButton' | 'message' | 'confirmText' | 'cancelText' | 'header' | 'onConfirm' | 'onCancel' | 'disableConfirmButton' >; type ModalState = { /** * The callback registered from the rendered message to call */ confirmCallback: null | (() => void); /** * Is confirm button disabled */ disableConfirmButton: boolean; }; class ConfirmModal extends Component { state: ModalState = { disableConfirmButton: !!this.props.disableConfirmButton, confirmCallback: null, }; confirming: boolean = false; handleClose = () => { const {disableConfirmButton, onCancel, closeModal} = this.props; onCancel?.(); this.setState({disableConfirmButton: disableConfirmButton ?? false}); // always reset `confirming` when modal visibility changes this.confirming = false; closeModal(); }; handleConfirm = () => { const {onConfirm, closeModal} = this.props; // `confirming` is used to ensure `onConfirm` or the confirm callback is // only called once if (!this.confirming) { onConfirm?.(); this.state.confirmCallback?.(); } this.setState({disableConfirmButton: true}); this.confirming = true; closeModal(); }; get confirmMessage() { const {message, renderMessage} = this.props; if (typeof renderMessage === 'function') { return renderMessage({ confirm: this.handleConfirm, close: this.handleClose, disableConfirmButton: (state: boolean) => this.setState({disableConfirmButton: state}), setConfirmCallback: (confirmCallback: () => void) => this.setState({confirmCallback}), }); } if (isValidElement(message)) { return message; } return (

{message}

); } render() { const { Header, Body, Footer, priority, confirmText, cancelText, header, renderConfirmButton, renderCancelButton, } = this.props; return ( {header &&
{header}
} {this.confirmMessage}
); } } export default Confirm;