Files
flipper/desktop/flipper-plugin/src/ui/Dialog.tsx
Lorenzo Blasa e80843d433 Modal visible -> open
Summary: The `visible` prop is marked as deprecated in favour of `open`

Reviewed By: passy

Differential Revision: D49226821

fbshipit-source-id: 4a4a7d03a1c8ff860c4e4cd02e19071185a8554e
2023-09-13 05:19:13 -07:00

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();
},
},
);
},
};