Summary: The `visible` prop is marked as deprecated in favour of `open` Reviewed By: passy Differential Revision: D49226821 fbshipit-source-id: 4a4a7d03a1c8ff860c4e4cd02e19071185a8554e
303 lines
7.6 KiB
TypeScript
303 lines
7.6 KiB
TypeScript
/**
|
|
* 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<T> = Promise<false | T> & {close: () => void};
|
|
|
|
type BaseDialogOptions = {
|
|
title: string;
|
|
okText?: string;
|
|
cancelText?: string;
|
|
width?: number;
|
|
okButtonProps?: ButtonProps;
|
|
cancelButtonProps?: ButtonProps;
|
|
};
|
|
|
|
const defaultWidth = 400;
|
|
|
|
export const Dialog = {
|
|
show<T>(
|
|
opts: BaseDialogOptions & {
|
|
defaultValue: T;
|
|
children: (currentValue: T, setValue: (v: T) => void) => React.ReactNode;
|
|
onConfirm?: (currentValue: T) => Promise<T>;
|
|
onValidate?: (value: T) => string;
|
|
},
|
|
): DialogResult<T> {
|
|
let cancel: () => void;
|
|
|
|
return Object.assign(
|
|
new Promise<false | T>((resolve) => {
|
|
const state = createState<T>(opts.defaultValue);
|
|
const submissionError = createState<string>('');
|
|
|
|
// 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 (
|
|
<Modal
|
|
title={opts.title}
|
|
open
|
|
okText={opts.okText}
|
|
cancelText={opts.cancelText}
|
|
onOk={async () => {
|
|
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}>
|
|
<Layout.Container gap>
|
|
{opts.children(currentValue, setCurrentValue)}
|
|
{currentError && <Alert type="error" message={currentError} />}
|
|
</Layout.Container>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
renderReactRoot((hide) => {
|
|
cancel = () => {
|
|
hide();
|
|
resolve(false);
|
|
};
|
|
return <DialogComponent onHide={hide} />;
|
|
});
|
|
}),
|
|
{
|
|
close() {
|
|
cancel();
|
|
},
|
|
},
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Shows an item in the modal stack, but without providing any further UI, like .show does.
|
|
*/
|
|
showModal<T = void>(
|
|
fn: (hide: (result?: T) => void) => React.ReactElement,
|
|
): DialogResult<T> {
|
|
let cancel: () => void;
|
|
|
|
return Object.assign(
|
|
new Promise<false | T>((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<true>;
|
|
} & BaseDialogOptions): DialogResult<true> {
|
|
return Dialog.show<true>({
|
|
...rest,
|
|
defaultValue: true,
|
|
children: () => message,
|
|
onConfirm: onConfirm,
|
|
});
|
|
},
|
|
|
|
alert({
|
|
message,
|
|
type,
|
|
...rest
|
|
}: {
|
|
message: React.ReactNode;
|
|
type: 'info' | 'error' | 'warning' | 'success';
|
|
} & BaseDialogOptions): Promise<void> & {close(): void} {
|
|
let modalRef: ReturnType<(typeof Modal)['info']>;
|
|
return Object.assign(
|
|
new Promise<void>((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<string>;
|
|
}): DialogResult<string> {
|
|
return Dialog.show<string>({
|
|
...rest,
|
|
defaultValue: defaultValue ?? '',
|
|
children: (value, onChange) => (
|
|
<Layout.Container gap>
|
|
<Typography.Text>{message}</Typography.Text>
|
|
<Input value={value} onChange={(e) => onChange(e.target.value)} />
|
|
</Layout.Container>
|
|
),
|
|
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<string>;
|
|
}): DialogResult<string | false> {
|
|
return Dialog.show<string>({
|
|
...rest,
|
|
defaultValue: undefined as any,
|
|
onValidate: (value) =>
|
|
value === undefined ? 'Please select an option' : '',
|
|
children: (value, onChange) => (
|
|
<Layout.Container gap style={{maxHeight: '50vh', overflow: 'auto'}}>
|
|
<Typography.Text>{message}</Typography.Text>
|
|
<Radio.Group
|
|
value={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.value);
|
|
}}>
|
|
<Space direction="vertical">
|
|
{options.map((o) => (
|
|
<Radio value={o.value} key={o.value}>
|
|
{o.label}
|
|
</Radio>
|
|
))}
|
|
</Space>
|
|
</Radio.Group>
|
|
</Layout.Container>
|
|
),
|
|
onConfirm,
|
|
});
|
|
},
|
|
|
|
select<T>({
|
|
defaultValue,
|
|
renderer,
|
|
...rest
|
|
}: {
|
|
defaultValue: T;
|
|
renderer: (
|
|
value: T,
|
|
onChange: (newValue: T) => void,
|
|
onCancel: () => void,
|
|
) => React.ReactElement;
|
|
} & BaseDialogOptions): DialogResult<false | T> {
|
|
const handle = Dialog.show<T>({
|
|
...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<void>((resolve) => {
|
|
renderReactRoot((hide) => {
|
|
cancel = () => {
|
|
hide();
|
|
resolve();
|
|
};
|
|
return (
|
|
<Modal
|
|
title={title ?? 'Loading...'}
|
|
open
|
|
footer={null}
|
|
width={width ?? defaultWidth}
|
|
closable={false}>
|
|
<Layout.Container gap center>
|
|
<Spinner />
|
|
{message}
|
|
</Layout.Container>
|
|
</Modal>
|
|
);
|
|
});
|
|
}),
|
|
{
|
|
close() {
|
|
cancel();
|
|
},
|
|
},
|
|
);
|
|
},
|
|
};
|