Introduce Dialog abstraction
Summary: Introduce convenience abstractions to make it easier to manage dialogs imperatively, by promisyfying common dialog abstractions. Reviewed By: jknoxville, nikoant Differential Revision: D29790462 fbshipit-source-id: c092c15cf569ec353b9c1042f25cd67e6c76db01
This commit is contained in:
committed by
Facebook GitHub Bot
parent
9c4deb3501
commit
f74029699f
@@ -17,9 +17,7 @@ import {Group, SUPPORTED_GROUPS} from './reducers/supportForm';
|
||||
import {Store} from './reducers/index';
|
||||
import {importDataToStore} from './utils/exportData';
|
||||
import {selectPlugin} from './reducers/connections';
|
||||
import {Layout, renderReactRoot} from 'flipper-plugin';
|
||||
import React, {useState} from 'react';
|
||||
import {Alert, Input, Modal} from 'antd';
|
||||
import {Dialog} from 'flipper-plugin';
|
||||
import {handleOpenPluginDeeplink} from './dispatcher/handleOpenPluginDeeplink';
|
||||
|
||||
const UNKNOWN = 'Unknown deeplink';
|
||||
@@ -118,40 +116,13 @@ export const uriComponents = (url: string): Array<string> => {
|
||||
};
|
||||
|
||||
export function openDeeplinkDialog(store: Store) {
|
||||
renderReactRoot((hide) => <TestDeeplinkDialog store={store} onHide={hide} />);
|
||||
}
|
||||
|
||||
function TestDeeplinkDialog({
|
||||
store,
|
||||
onHide,
|
||||
}: {
|
||||
store: Store;
|
||||
onHide: () => void;
|
||||
}) {
|
||||
const [query, setQuery] = useState('flipper://');
|
||||
const [error, setError] = useState('');
|
||||
return (
|
||||
<Modal
|
||||
title="Open deeplink..."
|
||||
visible
|
||||
onOk={() => {
|
||||
handleDeeplink(store, query)
|
||||
.then(onHide)
|
||||
.catch((e) => {
|
||||
setError(`Failed to handle deeplink '${query}': ${e}`);
|
||||
Dialog.prompt({
|
||||
title: 'Open deeplink',
|
||||
message: 'Enter a deeplink:',
|
||||
defaultValue: 'flipper://',
|
||||
onConfirm: async (deeplink) => {
|
||||
await handleDeeplink(store, deeplink);
|
||||
return deeplink;
|
||||
},
|
||||
});
|
||||
}}
|
||||
onCancel={onHide}>
|
||||
<Layout.Container gap>
|
||||
<>Enter a deeplink to test it:</>
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(v) => {
|
||||
setQuery(v.target.value);
|
||||
}}
|
||||
/>
|
||||
{error && <Alert type="error" message={error} />}
|
||||
</Layout.Container>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ test('Correct top level API exposed', () => {
|
||||
"DataSource",
|
||||
"DataTable",
|
||||
"DetailSidebar",
|
||||
"Dialog",
|
||||
"ElementsInspector",
|
||||
"Layout",
|
||||
"MarkerTimeline",
|
||||
|
||||
@@ -112,7 +112,7 @@ export {
|
||||
} from './ui/data-inspector/DataDescription';
|
||||
export {MarkerTimeline} from './ui/MarkerTimeline';
|
||||
export {DataInspector} from './ui/data-inspector/DataInspector';
|
||||
|
||||
export {Dialog} from './ui/Dialog';
|
||||
export {
|
||||
ElementsInspector,
|
||||
Element as ElementsInspectorElement,
|
||||
|
||||
132
desktop/flipper-plugin/src/ui/Dialog.tsx
Normal file
132
desktop/flipper-plugin/src/ui/Dialog.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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, Input, Modal, Typography} from 'antd';
|
||||
import {Atom, createState, useValue} from '../state/atom';
|
||||
import React from 'react';
|
||||
import {renderReactRoot} from '../utils/renderReactRoot';
|
||||
import {Layout} from './Layout';
|
||||
|
||||
type DialogResult<T> = Promise<false | T> & {close: () => void};
|
||||
|
||||
type BaseDialogOptions = {
|
||||
title: string;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
};
|
||||
|
||||
export const Dialog = {
|
||||
show<T>(
|
||||
opts: BaseDialogOptions & {
|
||||
children: React.ReactNode;
|
||||
onConfirm: () => Promise<T>;
|
||||
},
|
||||
): DialogResult<T> {
|
||||
let cancel: () => void;
|
||||
|
||||
return Object.assign(
|
||||
new Promise<false | T>((resolve) => {
|
||||
renderReactRoot((hide) => {
|
||||
const submissionError = createState<string>('');
|
||||
cancel = () => {
|
||||
hide();
|
||||
resolve(false);
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
title={opts.title}
|
||||
visible
|
||||
okText={opts.okText}
|
||||
cancelText={opts.cancelText}
|
||||
onOk={async () => {
|
||||
try {
|
||||
const value = await opts.onConfirm();
|
||||
hide();
|
||||
resolve(value);
|
||||
} catch (e) {
|
||||
submissionError.set(e.toString());
|
||||
}
|
||||
}}
|
||||
onCancel={cancel}
|
||||
width={400}>
|
||||
<Layout.Container gap>
|
||||
{opts.children}
|
||||
<SubmissionError submissionError={submissionError} />
|
||||
</Layout.Container>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
}),
|
||||
{
|
||||
close() {
|
||||
cancel();
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
confirm({
|
||||
message,
|
||||
...rest
|
||||
}: {
|
||||
message: string | React.ReactElement;
|
||||
} & BaseDialogOptions): DialogResult<true> {
|
||||
return Dialog.show<true>({
|
||||
...rest,
|
||||
children: message,
|
||||
onConfirm: async () => true,
|
||||
});
|
||||
},
|
||||
|
||||
prompt({
|
||||
message,
|
||||
defaultValue,
|
||||
onConfirm,
|
||||
...rest
|
||||
}: BaseDialogOptions & {
|
||||
message: string | React.ReactElement;
|
||||
defaultValue?: string;
|
||||
onConfirm?: (value: string) => Promise<string>;
|
||||
}): DialogResult<string> {
|
||||
const inputValue = createState(defaultValue ?? '');
|
||||
return Dialog.show<string>({
|
||||
...rest,
|
||||
children: (
|
||||
<>
|
||||
<Typography.Text>{message}</Typography.Text>
|
||||
<PromptInput inputValue={inputValue} />
|
||||
</>
|
||||
),
|
||||
onConfirm: async () => {
|
||||
const value = inputValue.get();
|
||||
if (onConfirm) {
|
||||
return await onConfirm(value);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -16,13 +16,12 @@ import {render, unmountComponentAtNode} from 'react-dom';
|
||||
*/
|
||||
export function renderReactRoot(
|
||||
handler: (unmount: () => void) => React.ReactElement,
|
||||
): void {
|
||||
): () => void {
|
||||
const div = document.body.appendChild(document.createElement('div'));
|
||||
render(
|
||||
handler(() => {
|
||||
const unmount = () => {
|
||||
unmountComponentAtNode(div);
|
||||
div.remove();
|
||||
}),
|
||||
div,
|
||||
);
|
||||
};
|
||||
render(handler(unmount), div);
|
||||
return unmount;
|
||||
}
|
||||
|
||||
@@ -895,6 +895,34 @@ Properties:
|
||||
* `type`: `default` or `dropdown. Defines the styling of the component. By default shows a list, but alternatively the items can be displayed in a drop down
|
||||
* `scrollable`: By default the data list will take all available space and scroll if items aren't otherwise visible. By setting `scrollable={false}` the list will only take its natural size
|
||||
|
||||
### Dialog
|
||||
|
||||
The `Dialog` namespace provides a set of utility to prompt the user with feedback of input. Rather than spawning dialogs by hand, the benefit of the `Dialog` utilities is that they all return promises capture the results.
|
||||
|
||||
The promises returned by `Dialog` will resolve to `false` if the user intentionally closed the dialog (typically by using cancel / escape / clicking the close button).
|
||||
|
||||
The promises returend by `Dialog` utilities will expose a `close()` method that can be used to programmatically close a dialog. In which case the pending promise will resolve to `false` as well.
|
||||
|
||||
General properties accepted by the `Dialog` utility:
|
||||
|
||||
* `title` - Overrides the title of the dialog, defaults to empty.
|
||||
* `width` - Overrides the default width (400) for dialogs. Number in pixels.
|
||||
* `okText` - Overrides the caption of the OK button
|
||||
* `cancelText` - Overrides the caption of the Cancel button
|
||||
|
||||
Available utilities
|
||||
|
||||
* `Dialog.confirm(options): Promise<boolean>`. Show a confirmation dialog to the user. Options:
|
||||
* `message`: Description of what the user is confirming.
|
||||
* `Dialog.prompt(options): Promise<string | false>`. Prompt the user for some input. Options:
|
||||
* `message`: Text accompanying the input
|
||||
* `defaultValue`
|
||||
* `onConfirm(value) => Promise<string>`. Can be used to transform the inputted value before resolving the prompt promise. If the handler throws, this will be shown as validation error in the dialog.
|
||||
* `Dialog.show<T>(options): Promise<T | false`. Low level building block to build dialogs. Options:
|
||||
* `children`: React Element to render as children of the dialog.
|
||||
* `onConfirm: () => Promise<T>`. Handler to handle the OK button, which should produce the value the `Dialog.show` call will resolve to.
|
||||
|
||||
|
||||
### NUX
|
||||
|
||||
An element that can be used to provide a New User eXperience: Hints that give a one time introduction to new features to the current user.
|
||||
|
||||
Reference in New Issue
Block a user