Introduce options dialog, cleaned up state managament

Summary: Made it easier to build 'pick' dialogs, and introduced Dialogs.options for a set of radio buttions

Reviewed By: timur-valiev

Differential Revision: D30424708

fbshipit-source-id: 98abd0d64f47c552c81053b4433e5fc524574145
This commit is contained in:
Michel Weststrate
2021-08-20 02:23:05 -07:00
committed by Facebook GitHub Bot
parent 419497db7e
commit 846246ffae

View File

@@ -7,8 +7,8 @@
* @format * @format
*/ */
import {Alert, Input, Modal, Typography} from 'antd'; import {Alert, Input, Modal, Radio, Space, Typography} from 'antd';
import {Atom, createState, useValue} from '../state/atom'; import {createState, useValue} from '../state/atom';
import React from 'react'; import React from 'react';
import {renderReactRoot} from '../utils/renderReactRoot'; import {renderReactRoot} from '../utils/renderReactRoot';
import {Layout} from './Layout'; import {Layout} from './Layout';
@@ -28,20 +28,31 @@ const defaultWidth = 400;
export const Dialog = { export const Dialog = {
show<T>( show<T>(
opts: BaseDialogOptions & { opts: BaseDialogOptions & {
children: React.ReactNode; defaultValue: T;
onConfirm: () => Promise<T>; children: (currentValue: T, setValue: (v: T) => void) => React.ReactNode;
onConfirm?: (currentValue: T) => Promise<T>;
onValidate?: (value: T) => string;
}, },
): DialogResult<T> { ): DialogResult<T> {
let cancel: () => void; let cancel: () => void;
return Object.assign( return Object.assign(
new Promise<false | T>((resolve) => { new Promise<false | T>((resolve) => {
renderReactRoot((hide) => { const state = createState<T>(opts.defaultValue);
const submissionError = createState<string>(''); const submissionError = createState<string>('');
cancel = () => {
hide(); // create inline component to subscribe to dialog state
resolve(false); 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 ( return (
<Modal <Modal
title={opts.title} title={opts.title}
@@ -50,21 +61,36 @@ export const Dialog = {
cancelText={opts.cancelText} cancelText={opts.cancelText}
onOk={async () => { onOk={async () => {
try { try {
const value = await opts.onConfirm(); const value = opts.onConfirm
hide(); ? await opts.onConfirm(currentValue)
: currentValue!;
onHide();
resolve(value); resolve(value);
} catch (e) { } catch (e) {
submissionError.set(e.toString()); submissionError.set(e.toString());
} }
}} }}
okButtonProps={{
disabled: opts.onValidate
? opts.onValidate(currentValue) !== ''
: false,
}}
onCancel={cancel} onCancel={cancel}
width={opts.width ?? defaultWidth}> width={opts.width ?? defaultWidth}>
<Layout.Container gap> <Layout.Container gap>
{opts.children} {opts.children(currentValue, setCurrentValue)}
<SubmissionError submissionError={submissionError} /> {currentError && <Alert type="error" message={currentError} />}
</Layout.Container> </Layout.Container>
</Modal> </Modal>
); );
};
renderReactRoot((hide) => {
cancel = () => {
hide();
resolve(false);
};
return <DialogComponent onHide={hide} />;
}); });
}), }),
{ {
@@ -85,8 +111,9 @@ export const Dialog = {
} & BaseDialogOptions): DialogResult<true> { } & BaseDialogOptions): DialogResult<true> {
return Dialog.show<true>({ return Dialog.show<true>({
...rest, ...rest,
children: message, defaultValue: true,
onConfirm: onConfirm ?? (async () => true), children: () => message,
onConfirm: onConfirm,
}); });
}, },
@@ -97,14 +124,22 @@ export const Dialog = {
}: { }: {
message: React.ReactNode; message: React.ReactNode;
type: 'info' | 'error' | 'warning' | 'success'; type: 'info' | 'error' | 'warning' | 'success';
} & BaseDialogOptions): Promise<void> { } & BaseDialogOptions): Promise<void> & {close(): void} {
return new Promise((resolve) => { let modalRef: ReturnType<typeof Modal['info']>;
Modal[type]({ return Object.assign(
afterClose: resolve, new Promise<void>((resolve) => {
content: message, modalRef = Modal[type]({
...rest, afterClose: resolve,
}); content: message,
}); ...rest,
});
}),
{
close() {
modalRef.destroy();
},
},
);
}, },
prompt({ prompt({
@@ -117,22 +152,53 @@ export const Dialog = {
defaultValue?: string; defaultValue?: string;
onConfirm?: (value: string) => Promise<string>; onConfirm?: (value: string) => Promise<string>;
}): DialogResult<string> { }): DialogResult<string> {
const inputValue = createState(defaultValue ?? '');
return Dialog.show<string>({ return Dialog.show<string>({
...rest, ...rest,
children: ( defaultValue: defaultValue ?? '',
<> children: (value, onChange) => (
<Layout.Container gap>
<Typography.Text>{message}</Typography.Text> <Typography.Text>{message}</Typography.Text>
<PromptInput inputValue={inputValue} /> <Input value={value} onChange={(e) => onChange(e.target.value)} />
</> </Layout.Container>
), ),
onConfirm: async () => { onValidate: (value) => (value ? '' : 'No input provided'),
const value = inputValue.get(); onConfirm,
if (onConfirm) { });
return await onConfirm(value); },
}
return value; options({
}, message,
onConfirm,
options,
...rest
}: BaseDialogOptions & {
message: React.ReactNode;
options: {label: string; value: string}[];
onConfirm?: (value: string) => Promise<string>;
}): DialogResult<string> {
return Dialog.show<string>({
...rest,
defaultValue: '',
onValidate: (value) => (value === '' ? '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,
}); });
}, },
@@ -177,20 +243,3 @@ export const Dialog = {
); );
}, },
}; };
function PromptInput({inputValue}: {inputValue: Atom<string>}) {
const currentValue = useValue(inputValue);
return (
<Input
value={currentValue}
onChange={(e) => {
inputValue.set(e.target.value);
}}
/>
);
}
function SubmissionError({submissionError}: {submissionError: Atom<string>}) {
const currentError = useValue(submissionError);
return currentError ? <Alert type="error" message={currentError} /> : null;
}