/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import {Alert, ButtonProps, Input, Modal, Radio, Space, Typography} from 'antd'; import {createState} from 'flipper-plugin-core'; import {useValue} from '../state/atom'; import React from 'react'; import {renderReactRoot} from '../utils/renderReactRoot'; import {Layout} from './Layout'; import {Spinner} from './Spinner'; export type DialogResult = Promise & {close: () => void}; type BaseDialogOptions = { title: string; okText?: string; cancelText?: string; width?: number; okButtonProps?: ButtonProps; cancelButtonProps?: ButtonProps; }; const defaultWidth = 400; export const Dialog = { show( opts: BaseDialogOptions & { defaultValue: T; children: (currentValue: T, setValue: (v: T) => void) => React.ReactNode; onConfirm?: (currentValue: T) => Promise; onValidate?: (value: T) => string; }, ): DialogResult { let cancel: () => void; return Object.assign( new Promise((resolve) => { const state = createState(opts.defaultValue); const submissionError = createState(''); // create inline component to subscribe to dialog state const DialogComponent = ({onHide}: {onHide: () => void}) => { const currentValue = useValue(state); const currentError = useValue(submissionError); const setCurrentValue = (v: T) => { state.set(v); if (opts.onValidate) { submissionError.set(opts.onValidate(v)); } }; return ( { try { const value = opts.onConfirm ? await opts.onConfirm(currentValue) : currentValue!; onHide(); resolve(value); } catch (e) { submissionError.set((e as Error).toString()); } }} okButtonProps={{ disabled: opts.onValidate ? !!opts.onValidate(currentValue) // non-falsy value means validation error : false, ...opts.okButtonProps, }} cancelButtonProps={opts.cancelButtonProps} onCancel={cancel} width={opts.width ?? defaultWidth}> {opts.children(currentValue, setCurrentValue)} {currentError && } ); }; renderReactRoot((hide) => { cancel = () => { hide(); resolve(false); }; return ; }); }), { close() { cancel(); }, }, ); }, /** * Shows an item in the modal stack, but without providing any further UI, like .show does. */ showModal( fn: (hide: (result?: T) => void) => React.ReactElement, ): DialogResult { let cancel: () => void; return Object.assign( new Promise((resolve) => { renderReactRoot((hide) => { cancel = () => { hide(); resolve(false); }; return fn((result?: T) => { hide(); resolve(result ?? false); }); }); }), { close() { cancel(); }, }, ); }, confirm({ message, onConfirm, ...rest }: { message: React.ReactNode; onConfirm?: () => Promise; } & BaseDialogOptions): DialogResult { return Dialog.show({ ...rest, defaultValue: true, children: () => message, onConfirm: onConfirm, }); }, alert({ message, type, ...rest }: { message: React.ReactNode; type: 'info' | 'error' | 'warning' | 'success'; } & BaseDialogOptions): Promise & {close(): void} { let modalRef: ReturnType<(typeof Modal)['info']>; return Object.assign( new Promise((resolve) => { modalRef = Modal[type]({ afterClose: resolve, content: message, ...rest, }); }), { close() { modalRef.destroy(); }, }, ); }, prompt({ message, defaultValue, onConfirm, ...rest }: BaseDialogOptions & { message: React.ReactNode; defaultValue?: string; onConfirm?: (value: string) => Promise; }): DialogResult { return Dialog.show({ ...rest, defaultValue: defaultValue ?? '', children: (value, onChange) => ( {message} onChange(e.target.value)} /> ), onValidate: (value) => (value ? '' : 'No input provided'), onConfirm, }); }, options({ message, onConfirm, options, ...rest }: BaseDialogOptions & { message: React.ReactNode; options: {label: string; value: string}[]; onConfirm?: (value: string) => Promise; }): DialogResult { return Dialog.show({ ...rest, defaultValue: undefined as any, onValidate: (value) => value === undefined ? 'Please select an option' : '', children: (value, onChange) => ( {message} { onChange(e.target.value); }}> {options.map((o) => ( {o.label} ))} ), onConfirm, }); }, select({ defaultValue, renderer, ...rest }: { defaultValue: T; renderer: ( value: T, onChange: (newValue: T) => void, onCancel: () => void, ) => React.ReactElement; } & BaseDialogOptions): DialogResult { const handle = Dialog.show({ ...rest, defaultValue, children: (currentValue, setValue): React.ReactElement => renderer(currentValue, setValue, () => handle.close()), }); return handle; }, loading({ title, message, width, }: { title?: string; message: React.ReactNode; width?: number; }) { let cancel: () => void; return Object.assign( new Promise((resolve) => { renderReactRoot((hide) => { cancel = () => { hide(); resolve(); }; return ( {message} ); }); }), { close() { cancel(); }, }, ); }, };