modals->fc transform remaining (#5278)

* remaining

* inline

* state =>

* tweaks

* simplfy wrapper

* extract prompt modal

* fix hotkey

* remove duplicate modal

* isJSON as object arg

* remove dispatch

* tweaks

* more tweaks

* fix nunjucks modal

* last tweaks
This commit is contained in:
Jack Kavanagh 2022-10-19 13:32:32 +01:00 committed by GitHub
parent 02c1c130ff
commit 92b8b3baf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 990 additions and 1314 deletions

View File

@ -1217,7 +1217,7 @@ export class UnconnectedCodeEditor extends Component<CodeEditorProps, State> {
_showFilterHelp() {
const isJSON = UnconnectedCodeEditor._isJSON(this.props.mode);
showModal(FilterHelpModal, isJSON);
showModal(FilterHelpModal, { isJSON });
}
render() {

View File

@ -89,7 +89,7 @@ export const RequestActionsDropdown = forwardRef<DropdownHandle, Props>(({
}, [handleDuplicateRequest, request]);
const generateCode = useCallback(() => {
showModal(GenerateCodeModal, request);
showModal(GenerateCodeModal, { request });
}, [request]);
const copyAsCurl = useCallback(async () => {

View File

@ -103,10 +103,6 @@ export const RequestGroupActionsDropdown = forwardRef<RequestGroupActionsDropdow
models.requestGroup.remove(requestGroup);
}, [requestGroup]);
const handleEditEnvironment = useCallback(() => {
showModal(EnvironmentEditModal, requestGroup);
}, [requestGroup]);
const handlePluginClick = useCallback(async ({ label, plugin, action }: RequestGroupAction) => {
setLoadingActions({ ...loadingActions, [label]: true });
@ -174,7 +170,7 @@ export const RequestGroupActionsDropdown = forwardRef<RequestGroupActionsDropdow
<i className="fa fa-copy" /> Duplicate
</DropdownItem>
<DropdownItem onClick={handleEditEnvironment}>
<DropdownItem onClick={() => showModal(EnvironmentEditModal, { requestGroup })}>
<i className="fa fa-code" /> Environment
</DropdownItem>

View File

@ -1,8 +1,6 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent, ReactNode } from 'react';
import React, { forwardRef, ReactNode, useImperativeHandle, useRef, useState } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import { type ModalHandle, Modal } from '../base/modal';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
@ -14,90 +12,61 @@ export interface AlertModalOptions {
okLabel?: string;
onConfirm?: () => void | Promise<void>;
}
type State = Omit<AlertModalOptions, 'onConfirm'>;
@autoBindMethodsForReact(AUTOBIND_CFG)
export class AlertModal extends PureComponent<{}, State> {
state: State = {
export interface AlertModalHandle {
show: (options: AlertModalOptions) => void;
hide: () => void;
}
export const AlertModal = forwardRef<AlertModalHandle, ModalProps>((_, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<AlertModalOptions>({
title: '',
message: '',
addCancel: false,
okLabel: '',
};
});
modal: ModalHandle | null = null;
_cancel: HTMLButtonElement | null = null;
_ok: HTMLButtonElement | null = null;
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: ({ title, message, addCancel, onConfirm, okLabel }) => {
setState({
title,
message,
addCancel,
okLabel,
onConfirm,
});
modalRef.current?.show();
},
}), []);
_okCallback?: (value: void | PromiseLike<void>) => void;
_okCallback2: AlertModalOptions['onConfirm'];
_setModalRef(modal: ModalHandle) {
this.modal = modal;
}
_handleOk() {
this.hide();
// TODO: unsound non-null assertion
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this._okCallback!();
if (typeof this._okCallback2 === 'function') {
this._okCallback2();
}
}
hide() {
this.modal?.hide();
}
setCancelRef(cancel: HTMLButtonElement) {
this._cancel = cancel;
}
setOkRef(ok: HTMLButtonElement) {
this._ok = ok;
}
show({ title, message, addCancel, onConfirm, okLabel }: AlertModalOptions) {
this.setState({
title,
message,
addCancel,
okLabel,
});
this.modal?.show();
// Need to do this after render because modal focuses itself too
setTimeout(() => {
this._cancel?.focus();
}, 100);
this._okCallback2 = onConfirm;
return new Promise<void>(resolve => {
this._okCallback = resolve;
});
}
render() {
const { message, title, addCancel, okLabel } = this.state;
return (
<Modal ref={this._setModalRef} skinny>
<ModalHeader>{title || 'Uh Oh!'}</ModalHeader>
<ModalBody className="wide pad">{message}</ModalBody>
<ModalFooter>
<div>
{addCancel ? (
<button className="btn" ref={this.setCancelRef} onClick={this.hide}>
Cancel
</button>
) : null}
<button className="btn" ref={this.setOkRef} onClick={this._handleOk}>
{okLabel || 'Ok'}
const { message, title, addCancel, okLabel } = state;
return (
<Modal ref={modalRef} skinny>
<ModalHeader>{title || 'Uh Oh!'}</ModalHeader>
<ModalBody className="wide pad">{message}</ModalBody>
<ModalFooter>
<div>
{addCancel ? (
<button className="btn" onClick={() => modalRef.current?.hide()}>
Cancel
</button>
</div>
</ModalFooter>
</Modal>
);
}
}
) : null}
<button
className="btn"
onClick={() => {
modalRef.current?.hide();
if (typeof state.onConfirm === 'function') {
state.onConfirm();
}
}}
>
{okLabel || 'Ok'}
</button>
</div>
</ModalFooter>
</Modal>
);
});
AlertModal.displayName = 'AlertModal';

View File

@ -1,118 +1,80 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent } from 'react';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import { type ModalHandle, Modal } from '../base/modal';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
interface State {
title: string;
message: string;
yesText: string;
noText: string;
loading: boolean;
onDone?: (success: boolean) => Promise<void>;
}
interface AskModalOptions {
export interface AskModalOptions {
title?: string;
message?: string;
onDone?: (success: boolean) => Promise<void>;
yesText?: string;
noText?: string;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
export class AskModal extends PureComponent<{}, State> {
state: State = {
export interface AskModalHandle {
show: (options: AskModalOptions) => void;
hide: () => void;
}
export const AskModal = forwardRef<AskModalHandle, ModalProps>((_, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<State>({
title: '',
message: '',
yesText: 'Yes',
noText: 'No',
loading: false,
};
onDone: async () => { },
});
modal: ModalHandle | null = null;
yesButton: HTMLButtonElement | null = null;
_doneCallback: AskModalOptions['onDone'];
_promiseCallback: (value: boolean | PromiseLike<boolean>) => void = () => {};
_setModalRef(modal: ModalHandle) {
this.modal = modal;
}
_setYesButtonRef(yesButton: HTMLButtonElement) {
this.yesButton = yesButton;
}
async _handleYes() {
this.setState({
loading: true,
});
// Wait for the callback to finish before closing
await this._doneCallback?.(true);
this._promiseCallback(true);
this.hide();
}
async _handleNo() {
this.hide();
await this._doneCallback?.(false);
this._promiseCallback(false);
}
hide() {
this.modal?.hide();
}
show({ title, message, onDone, yesText, noText }: AskModalOptions = {}) {
this._doneCallback = onDone;
this.setState({
title: title || 'Confirm',
message: message || 'No message provided',
yesText: yesText || 'Yes',
noText: noText || 'No',
loading: false,
});
this.modal?.show();
setTimeout(() => {
this.yesButton?.focus();
}, 100);
return new Promise<boolean>(resolve => {
this._promiseCallback = resolve;
});
}
render() {
const { message, title, yesText, noText, loading } = this.state;
return (
<Modal noEscape ref={this._setModalRef}>
<ModalHeader>{title || 'Confirm?'}</ModalHeader>
<ModalBody className="wide pad">{message}</ModalBody>
<ModalFooter>
<div>
<button className="btn" onClick={this._handleNo}>
{noText}
</button>
<button
ref={this._setYesButtonRef}
className="btn"
onClick={this._handleYes}
disabled={loading}
>
{loading && <i className="fa fa-refresh fa-spin" />} {yesText}
</button>
</div>
</ModalFooter>
</Modal>
);
}
}
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: ({ title, message, onDone, yesText, noText }) => {
setState({
title: title || 'Confirm',
message: message || 'No message provided',
yesText: yesText || 'Yes',
noText: noText || 'No',
onDone,
});
modalRef.current?.show();
},
}), []);
const { message, title, yesText, noText, onDone } = state;
return (
<Modal ref={modalRef} noEscape>
<ModalHeader>{title || 'Confirm?'}</ModalHeader>
<ModalBody className="wide pad">{message}</ModalBody>
<ModalFooter>
<div>
<button
className="btn"
onClick={() => {
modalRef.current?.hide();
onDone?.(false);
}}
>
{noText}
</button>
<button
className="btn"
onClick={() => {
modalRef.current?.hide();
onDone?.(true);
}}
>
{yesText}
</button>
</div>
</ModalFooter>
</Modal>
);
});
AskModal.displayName = 'AskModal';

View File

@ -1,14 +1,12 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent } from 'react';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import { NunjucksEnabledProvider } from '../../context/nunjucks/nunjucks-enabled-context';
import { CopyButton } from '../base/copy-button';
import { Dropdown } from '../base/dropdown/dropdown';
import { DropdownButton } from '../base/dropdown/dropdown-button';
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
import { DropdownItem } from '../base/dropdown/dropdown-item';
import { type ModalHandle, Modal } from '../base/modal';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
@ -24,7 +22,7 @@ const MODES: Record<string, string> = {
'text/html': 'HTML',
};
interface State {
interface CodePromptModalOptions {
title: string;
defaultValue: string;
submitName: string;
@ -34,11 +32,17 @@ interface State {
hideMode: boolean;
enableRender: boolean;
showCopyButton: boolean;
onChange: (value: string) => void;
onModeChange?: (value: string) => void;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
export class CodePromptModal extends PureComponent<{}, State> {
state: State = {
export interface CodePromptModalHandle {
show: (options: CodePromptModalOptions) => void;
hide: () => void;
}
export const CodePromptModal = forwardRef<CodePromptModalHandle, ModalProps>((_, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<CodePromptModalOptions>({
title: 'Not Set',
defaultValue: '',
submitName: 'Not Set',
@ -48,150 +52,117 @@ export class CodePromptModal extends PureComponent<{}, State> {
hideMode: false,
enableRender: false,
showCopyButton: false,
};
onChange: () => { },
onModeChange: () => { },
});
modal: ModalHandle | null = null;
_onModeChange: Function = () => {};
_onChange: Function = () => {};
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: options => {
const realMode = typeof options.mode === 'string' ? options.mode : 'text/plain';
setState(state => ({
...options,
mode: realMode || state.mode || 'text/plain',
}));
modalRef.current?.show();
},
}), []);
_setModalRef(modal: ModalHandle) {
this.modal = modal;
}
const {
submitName,
title,
placeholder,
defaultValue,
hint,
mode,
hideMode,
enableRender,
showCopyButton,
onChange,
} = state;
_handleChange(value: any) {
this._onChange(value);
}
_handleChangeMode(mode: any) {
this.setState({ mode });
this._onModeChange?.(mode);
}
hide() {
this.modal?.hide();
}
show(options: any) {
const {
title,
defaultValue,
submitName,
placeholder,
hint,
mode,
hideMode,
enableRender,
onChange,
onModeChange,
showCopyButton,
} = options;
this._onChange = onChange;
this._onModeChange = onModeChange;
const realMode = typeof mode === 'string' ? mode : 'text/plain';
this.setState({
title,
defaultValue,
submitName,
placeholder,
hint,
enableRender,
hideMode,
showCopyButton,
mode: realMode || this.state.mode || 'text/plain',
});
this.modal?.show();
}
render() {
const {
} = this.props;
const {
submitName,
title,
placeholder,
defaultValue,
hint,
mode,
hideMode,
enableRender,
showCopyButton,
} = this.state;
return (
<Modal ref={this._setModalRef} tall>
<ModalHeader>{title}</ModalHeader>
<ModalBody
noScroll
className="wide tall"
style={
showCopyButton
? {
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr)',
gridTemplateRows: 'auto minmax(0, 1fr)',
}
: {
minHeight: '10rem',
}
}
>
<NunjucksEnabledProvider disable={!enableRender}>
{showCopyButton ? (
<div className="pad-top-sm pad-right-sm">
<CopyButton content={defaultValue} className="pull-right" />
</div>
) : null}
{mode === 'text/x-markdown' ? (
<div className="pad-sm tall">
<MarkdownEditor
tall
return (
<Modal ref={modalRef} tall>
<ModalHeader>{title}</ModalHeader>
<ModalBody
noScroll
className="wide tall"
style={
showCopyButton
? {
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr)',
gridTemplateRows: 'auto minmax(0, 1fr)',
}
: {
minHeight: '10rem',
}
}
>
<NunjucksEnabledProvider disable={!enableRender}>
{showCopyButton ? (
<div className="pad-top-sm pad-right-sm">
<CopyButton content={defaultValue} className="pull-right" />
</div>
) : null}
{mode === 'text/x-markdown' ? (
<div className="pad-sm tall">
<MarkdownEditor
tall
defaultValue={defaultValue}
placeholder={placeholder}
onChange={onChange}
mode={mode}
/>
</div>
) : (
<div className="pad-sm pad-bottom tall">
<div className="form-control form-control--outlined form-control--tall tall">
<CodeEditor
hideLineNumbers
manualPrettify
className="tall"
defaultValue={defaultValue}
placeholder={placeholder}
onChange={this._handleChange}
onChange={onChange}
mode={mode}
enableNunjucks
/>
</div>
) : (
<div className="pad-sm pad-bottom tall">
<div className="form-control form-control--outlined form-control--tall tall">
<CodeEditor
hideLineNumbers
manualPrettify
className="tall"
defaultValue={defaultValue}
placeholder={placeholder}
onChange={this._handleChange}
mode={mode}
enableNunjucks
/>
</div>
</div>
)}
</NunjucksEnabledProvider>
</ModalBody>
<ModalFooter>
{!hideMode ? (
<Dropdown>
<DropdownButton className="btn btn--clicky margin-left-sm">
</div>
)}
</NunjucksEnabledProvider>
</ModalBody>
<ModalFooter>
{!hideMode ? (
<Dropdown>
<DropdownButton className="btn btn--clicky margin-left-sm">
{MODES[mode]}
<i className="fa fa-caret-down space-left" />
</DropdownButton>
<DropdownDivider>Editor Syntax</DropdownDivider>
{Object.keys(MODES).map(mode => (
<DropdownItem
key={mode}
onClick={() => {
setState(state => ({ ...state, mode }));
state.onModeChange?.(mode);
}}
>
<i className="fa fa-code" />
{MODES[mode]}
<i className="fa fa-caret-down space-left" />
</DropdownButton>
<DropdownDivider>Editor Syntax</DropdownDivider>
{Object.keys(MODES).map(mode => (
<DropdownItem key={mode} onClick={() => this._handleChangeMode(mode)}>
<i className="fa fa-code" />
{MODES[mode]}
</DropdownItem>
))}
</Dropdown>
) : null}
<div className="margin-left faint italic txt-sm">{hint ? `* ${hint}` : ''}</div>
<button className="btn" onClick={this.hide}>
{submitName || 'Submit'}
</button>
</ModalFooter>
</Modal>
);
}
}
</DropdownItem>
))}
</Dropdown>
) : null}
<div className="margin-left faint italic txt-sm">{hint ? `* ${hint}` : ''}</div>
<button className="btn" onClick={() => modalRef.current?.hide()}>
{submitName || 'Submit'}
</button>
</ModalFooter>
</Modal>
);
});
CodePromptModal.displayName = 'CodePromptModal';

View File

@ -1,112 +1,86 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent } from 'react';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import * as models from '../../../models/index';
import { RequestGroup } from '../../../models/request-group';
import { type ModalHandle, Modal } from '../base/modal';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
import { EnvironmentEditor } from '../editors/environment-editor';
interface Props {
onChange: Function;
}
interface State {
requestGroup: RequestGroup | null;
isValid: boolean;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
export class EnvironmentEditModal extends PureComponent<Props, State> {
state: State = {
export interface EnvironmentEditModalOptions {
requestGroup: RequestGroup;
}
export interface EnvironmentEditModalHandle {
show: (options: EnvironmentEditModalOptions) => void;
hide: () => void;
}
export const EnvironmentEditModal = forwardRef<EnvironmentEditModalHandle, ModalProps>((props, ref) => {
const modalRef = useRef<ModalHandle>(null);
const editorRef = useRef<EnvironmentEditor>(null);
const [state, setState] = useState<State>({
requestGroup: null,
isValid: true,
};
});
modal: ModalHandle | null = null;
_envEditor: EnvironmentEditor | null = null;
_setModalRef(modal: ModalHandle) {
this.modal = modal;
}
_setEditorRef(envEditor: EnvironmentEditor) {
this._envEditor = envEditor;
}
_saveChanges() {
if (!this._envEditor?.isValid()) {
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: ({ requestGroup }) => {
setState(state => ({ ...state, requestGroup }));
modalRef.current?.show();
},
}), []);
const didChange = () => {
const isValid = editorRef.current?.isValid() || false;
setState({ isValid, requestGroup });
if (!isValid) {
return;
}
let patch;
try {
const data = this._envEditor.getValue();
patch = {
environment: data && data.object,
environmentPropertyOrder: data && data.propertyOrder,
};
const data = editorRef.current?.getValue();
if (state.requestGroup && data) {
models.requestGroup.update(state.requestGroup, {
environment: data.object,
environmentPropertyOrder: data.propertyOrder,
});
}
} catch (err) {
// Invalid JSON probably
return;
}
const { requestGroup } = this.state;
this.props.onChange(Object.assign({}, requestGroup, patch));
}
_didChange() {
this._saveChanges();
const isValid = Boolean(this._envEditor?.isValid());
if (this.state.isValid !== isValid) {
this.setState({ isValid });
}
}
show(requestGroup: RequestGroup) {
this.setState({ requestGroup });
this.modal?.show();
}
hide() {
this.modal?.hide();
}
render() {
const {
...extraProps
} = this.props;
const { requestGroup, isValid } = this.state;
const environmentInfo = {
object: requestGroup ? requestGroup.environment : {},
propertyOrder: requestGroup && requestGroup.environmentPropertyOrder,
};
return (
<Modal ref={this._setModalRef} tall {...extraProps}>
<ModalHeader>Environment Overrides (JSON Format)</ModalHeader>
<ModalBody noScroll className="pad-top-sm">
<EnvironmentEditor
ref={this._setEditorRef}
key={requestGroup ? requestGroup._id : 'n/a'}
environmentInfo={environmentInfo}
didChange={this._didChange}
/>
</ModalBody>
<ModalFooter>
<div className="margin-left italic txt-sm">
* Used to override data in the global environment
</div>
<button className="btn" disabled={!isValid} onClick={this.hide}>
Done
</button>
</ModalFooter>
</Modal>
);
}
}
};
const { requestGroup, isValid } = state;
const environmentInfo = {
object: requestGroup ? requestGroup.environment : {},
propertyOrder: requestGroup && requestGroup.environmentPropertyOrder,
};
return (
<Modal ref={modalRef} tall {...props}>
<ModalHeader>Environment Overrides (JSON Format)</ModalHeader>
<ModalBody noScroll className="pad-top-sm">
<EnvironmentEditor
ref={editorRef}
key={requestGroup ? requestGroup._id : 'n/a'}
environmentInfo={environmentInfo}
didChange={didChange}
/>
</ModalBody>
<ModalFooter>
<div className="margin-left italic txt-sm">
* Used to override data in the global environment
</div>
<button className="btn" disabled={!isValid} onClick={() => modalRef.current?.hide()}>
Done
</button>
</ModalFooter>
</Modal >
);
});
EnvironmentEditModal.displayName = 'EnvironmentEditModal';

View File

@ -1,92 +1,67 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent } from 'react';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import { type ModalHandle, Modal } from '../base/modal';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
// NOTE: this is only used by the plugin api
export interface ErrorModalOptions {
title?: string;
error?: Error | null;
addCancel?: boolean;
message?: string;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
export class ErrorModal extends PureComponent<{}, ErrorModalOptions> {
modal: ModalHandle | null = null;
_okCallback: (value?: unknown) => void = () => {};
state: ErrorModalOptions = {
export interface ErrorModalHandle {
show: (options: ErrorModalOptions) => void;
hide: () => void;
}
export const ErrorModal = forwardRef<ErrorModalHandle, ModalProps>((_, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<ErrorModalOptions>({
title: '',
error: null,
message: '',
addCancel: false,
};
});
_setModalRef(modal: ModalHandle) {
this.modal = modal;
}
_handleOk() {
this.hide();
this._okCallback();
}
hide() {
this.modal?.hide();
}
show(options: ErrorModalOptions = {}) {
const { title, error, addCancel, message } = options;
this.setState({
title,
error,
addCancel,
message,
});
this.modal?.show();
console.log('[ErrorModal]', error);
return new Promise(resolve => {
this._okCallback = resolve;
});
}
render() {
const { error, title, addCancel } = this.state;
const message = this.state.message || error?.message;
return (
<Modal ref={this._setModalRef}>
<ModalHeader>{title || 'Uh Oh!'}</ModalHeader>
<ModalBody className="wide pad">
{message ? <div className="notice error pre">{message}</div> : null}
{error && (
<details>
<summary>Stack trace</summary>
<pre className="pad-top-sm force-wrap selectable">
<code className="wide">{error.stack}</code>
</pre>
</details>
)}
</ModalBody>
<ModalFooter>
<div>
{addCancel ? (
<button className="btn" onClick={this.hide}>
Cancel
</button>
) : null}
<button className="btn" onClick={this._handleOk}>
Ok
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: options => {
setState(options);
modalRef.current?.show();
},
}), []);
const { error, title, addCancel } = state;
const message = state.message || error?.message;
return (
<Modal ref={modalRef}>
<ModalHeader>{title || 'Uh Oh!'}</ModalHeader>
<ModalBody className="wide pad">
{message ? <div className="notice error pre">{message}</div> : null}
{error && (
<details>
<summary>Stack trace</summary>
<pre className="pad-top-sm force-wrap selectable">
<code className="wide">{error.stack}</code>
</pre>
</details>
)}
</ModalBody>
<ModalFooter>
<div>
{addCancel ? (
<button className="btn" onClick={() => modalRef.current?.hide()}>
Cancel
</button>
</div>
</ModalFooter>
</Modal>
);
}
}
) : null}
<button className="btn" onClick={() => modalRef.current?.hide()}>
Ok
</button>
</div>
</ModalFooter>
</Modal>
);
});
ErrorModal.displayName = 'ErrorModal';

View File

@ -1,14 +1,11 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { AUTOBIND_CFG } from '../../../common/constants';
import * as models from '../../../models';
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
import { isRequest, Request } from '../../../models/request';
import { isRequestGroup, RequestGroup } from '../../../models/request-group';
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
import { RootState } from '../../redux/modules';
import { exportRequestsToFile } from '../../redux/modules/global';
import { selectSidebarChildren } from '../../redux/sidebar-selectors';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
@ -25,104 +22,26 @@ export interface Node {
selectedRequests: number;
}
type Props = ModalProps & ReturnType<typeof mapStateToProps>;
interface State {
export interface State {
treeRoot: Node | null;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
export class ExportRequestsModalClass extends PureComponent<Props, State> {
modal: ModalHandle | null = null;
state: State = {
treeRoot: null,
};
setModalRef(modal: ModalHandle) {
if (modal != null) {
this.modal = modal;
}
}
show() {
this.modal?.show();
this.createTree();
}
hide() {
this.modal?.hide();
}
handleExport() {
const { treeRoot } = this.state;
if (treeRoot == null || treeRoot.selectedRequests === 0) {
return;
}
const exportedRequestIds = this.getSelectedRequestIds(treeRoot);
exportRequestsToFile(exportedRequestIds);
this.hide();
}
getSelectedRequestIds(node: Node): string[] {
const docIsRequest = isRequest(node.doc) || isWebSocketRequest(node.doc) || isGrpcRequest(node.doc);
if (docIsRequest && node.selectedRequests === node.totalRequests) {
return [node.doc._id];
}
const requestIds: string[] = [];
for (const child of node.children) {
const reqIds = this.getSelectedRequestIds(child);
requestIds.push(...reqIds);
}
return requestIds;
}
createTree() {
const { sidebarChildren } = this.props;
const childObjects = sidebarChildren.all;
const children: Node[] = childObjects.map(child => this.createNode(child));
const totalRequests = children
.map(child => child.totalRequests)
.reduce((acc, totalRequests) => acc + totalRequests, 0);
// @ts-expect-error -- TSCONVERSION missing property
const rootFolder: RequestGroup = {
...models.requestGroup.init(),
_id: 'all',
type: models.requestGroup.type,
name: 'All requests',
parentId: '',
modified: 0,
created: 0,
};
this.setState({
treeRoot: {
doc: rootFolder,
collapsed: false,
children: children,
totalRequests: totalRequests,
selectedRequests: totalRequests, // Default select all
},
});
}
createNode(item: Record<string, any>): Node {
const children: Node[] = item.children.map((child: Record<string, any>) => this.createNode(child));
export interface ExportRequestsModalHandle {
show: () => void;
hide: () => void;
}
export const ExportRequestsModal = forwardRef<ExportRequestsModalHandle, ModalProps>((props, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<State>({ treeRoot: null });
const sidebarChildren = useSelector(selectSidebarChildren);
const createNode = useCallback((item: Record<string, any>): Node => {
const children: Node[] = item.children.map((child: Record<string, any>) => createNode(child));
let totalRequests = children
.map(child => child.totalRequests)
.reduce((acc, totalRequests) => acc + totalRequests, 0);
const docIsRequest = isRequest(item.doc) || isWebSocketRequest(item.doc) || isGrpcRequest(item.doc);
if (docIsRequest) {
totalRequests++;
}
return {
doc: item.doc,
collapsed: false,
@ -130,79 +49,64 @@ export class ExportRequestsModalClass extends PureComponent<Props, State> {
totalRequests: totalRequests,
selectedRequests: totalRequests, // Default select all
};
}
}, []);
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: () => {
modalRef.current?.show();
const childObjects = sidebarChildren.all;
const children: Node[] = childObjects.map(child => createNode(child));
const totalRequests = children
.map(child => child.totalRequests)
.reduce((acc, totalRequests) => acc + totalRequests, 0);
handleSetRequestGroupCollapsed(requestGroupId: string, isCollapsed: boolean) {
const { treeRoot } = this.state;
if (treeRoot == null) {
return;
// @ts-expect-error -- TSCONVERSION missing property
const rootFolder: RequestGroup = {
...models.requestGroup.init(),
_id: 'all',
type: models.requestGroup.type,
name: 'All requests',
parentId: '',
modified: 0,
created: 0,
};
setState({
treeRoot: {
doc: rootFolder,
collapsed: false,
children: children,
totalRequests: totalRequests,
selectedRequests: totalRequests, // Default select all
},
});
},
}), [createNode, sidebarChildren.all]);
const getSelectedRequestIds = (node: Node): string[] => {
const docIsRequest = isRequest(node.doc) || isWebSocketRequest(node.doc) || isGrpcRequest(node.doc);
if (docIsRequest && node.selectedRequests === node.totalRequests) {
return [node.doc._id];
}
const found = this.setRequestGroupCollapsed(treeRoot, isCollapsed, requestGroupId);
if (!found) {
return;
}
this.setState({
treeRoot: { ...treeRoot },
});
}
handleSetItemSelected(itemId: string, isSelected: boolean) {
const { treeRoot } = this.state;
if (treeRoot == null) {
return;
}
const found = this.setItemSelected(treeRoot, isSelected, itemId);
if (!found) {
return;
}
this.setState({
treeRoot: { ...treeRoot },
});
}
setRequestGroupCollapsed(node: Node, isCollapsed: boolean, requestGroupId: string) {
if (!isRequestGroup(node.doc)) {
return false;
}
if (node.doc._id === requestGroupId) {
node.collapsed = isCollapsed;
return true;
}
const requestIds: string[] = [];
for (const child of node.children) {
const found = this.setRequestGroupCollapsed(child, isCollapsed, requestGroupId);
if (found) {
return true;
}
const reqIds = getSelectedRequestIds(child);
requestIds.push(...reqIds);
}
return requestIds;
};
return false;
}
setItemSelected(node: Node, isSelected: boolean, id?: string) {
const setItemSelected = (node: Node, isSelected: boolean, id?: string) => {
if (id == null || node.doc._id === id) {
// Switch the flags of all children in this subtree.
for (const child of node.children) {
this.setItemSelected(child, isSelected);
setItemSelected(child, isSelected);
}
node.selectedRequests = isSelected ? node.totalRequests : 0;
return true;
}
for (const child of node.children) {
const found = this.setItemSelected(child, isSelected, id);
const found = setItemSelected(child, isSelected, id);
if (found) {
node.selectedRequests = node.children
.map(ch => ch.selectedRequests)
@ -210,42 +114,86 @@ export class ExportRequestsModalClass extends PureComponent<Props, State> {
return true;
}
}
return false;
}
};
const handleSetItemSelected = (itemId: string, isSelected: boolean) => {
const { treeRoot } = state;
if (treeRoot == null) {
return;
}
const found = setItemSelected(treeRoot, isSelected, itemId);
if (!found) {
return;
}
setState({ treeRoot: { ...treeRoot } });
};
const handleSetRequestGroupCollapsed = (requestGroupId: string, isCollapsed: boolean) => {
const { treeRoot } = state;
if (treeRoot == null) {
return;
}
const found = setRequestGroupCollapsed(treeRoot, isCollapsed, requestGroupId);
if (!found) {
return;
}
setState({ treeRoot: { ...treeRoot } });
};
const setRequestGroupCollapsed = (node: Node, isCollapsed: boolean, requestGroupId: string) => {
if (!isRequestGroup(node.doc)) {
return false;
}
if (node.doc._id === requestGroupId) {
node.collapsed = isCollapsed;
return true;
}
for (const child of node.children) {
const found = setRequestGroupCollapsed(child, isCollapsed, requestGroupId);
if (found) {
return true;
}
}
return false;
};
const handleExport = () => {
const { treeRoot } = state;
if (treeRoot == null || treeRoot.selectedRequests === 0) {
return;
}
const exportedRequestIds = getSelectedRequestIds(treeRoot);
render() {
const { treeRoot } = this.state;
const isExportDisabled = treeRoot != null ? treeRoot.selectedRequests === 0 : false;
return (
<Modal ref={this.setModalRef} tall {...this.props}>
<ModalHeader>Select Requests to Export</ModalHeader>
<ModalBody>
<div className="requests-tree">
<Tree
root={treeRoot}
handleSetRequestGroupCollapsed={this.handleSetRequestGroupCollapsed}
handleSetItemSelected={this.handleSetItemSelected}
/>
</div>
</ModalBody>
<ModalFooter>
<div>
<button className="btn" onClick={this.hide}>
Cancel
</button>
<button className="btn" onClick={this.handleExport} disabled={isExportDisabled}>
Export
</button>
</div>
</ModalFooter>
</Modal>
);
}
}
exportRequestsToFile(exportedRequestIds);
modalRef.current?.hide();
};
const mapStateToProps = (state: RootState) => ({
sidebarChildren: selectSidebarChildren(state),
const { treeRoot } = state;
const isExportDisabled = treeRoot != null ? treeRoot.selectedRequests === 0 : false;
return (
<Modal ref={modalRef} tall {...props}>
<ModalHeader>Select Requests to Export</ModalHeader>
<ModalBody>
<div className="requests-tree">
<Tree
root={treeRoot}
handleSetRequestGroupCollapsed={handleSetRequestGroupCollapsed}
handleSetItemSelected={handleSetItemSelected}
/>
</div>
</ModalBody>
<ModalFooter>
<div>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Cancel
</button>
<button
className="btn"
onClick={handleExport}
disabled={isExportDisabled}
>
Export
</button>
</div>
</ModalFooter>
</Modal>
);
});
export const ExportRequestsModal = connect(mapStateToProps, null, null, { forwardRef:true })(ExportRequestsModalClass);
ExportRequestsModal.displayName = 'ExportRequestsModal';

View File

@ -1,16 +1,10 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { FC, PureComponent } from 'react';
import React, { FC, forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import { Link } from '../base/link';
import { type ModalHandle, Modal } from '../base/modal';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalHeader } from '../base/modal-header';
interface State {
isJSON: boolean;
}
interface HelpExample {
code: string;
description: string;
@ -65,37 +59,38 @@ const XPathHelp: FC = () => (
/>
</ModalBody>
);
@autoBindMethodsForReact(AUTOBIND_CFG)
export class FilterHelpModal extends PureComponent<{}, State> {
state: State = {
isJSON: true,
};
modal: ModalHandle | null = null;
_setModalRef(modal: ModalHandle) {
this.modal = modal;
}
show(isJSON: boolean) {
this.setState({ isJSON });
this.modal?.show();
}
hide() {
this.modal?.hide();
}
render() {
const { isJSON } = this.state;
const isXPath = !isJSON;
return (
<Modal ref={this._setModalRef}>
<ModalHeader>Response Filtering Help</ModalHeader>
{isJSON ? <JSONPathHelp /> : null}
{isXPath ? <XPathHelp /> : null}
</Modal>
);
}
interface FilterHelpModalOptions {
isJSON: boolean;
}
export interface FilterHelpModalHandle {
show: (options: FilterHelpModalOptions) => void;
hide: () => void;
}
export const FilterHelpModal = forwardRef<FilterHelpModalHandle, ModalProps>((_, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<FilterHelpModalOptions>({
isJSON: true,
});
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: options => {
const { isJSON } = options;
setState({ isJSON });
modalRef.current?.show();
},
}), []);
const { isJSON } = state;
const isXPath = !isJSON;
return (
<Modal ref={modalRef}>
<ModalHeader>Response Filtering Help</ModalHeader>
{isJSON ? <JSONPathHelp /> : null}
{isXPath ? <XPathHelp /> : null}
</Modal>
);
});
FilterHelpModal.displayName = 'FilterHelpModal';

View File

@ -1,8 +1,6 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import HTTPSnippet, { HTTPSnippetClient, HTTPSnippetTarget } from 'httpsnippet';
import React, { PureComponent } from 'react';
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import { exportHarRequest } from '../../../common/har';
import { Request } from '../../../models/request';
import { CopyButton } from '../base/copy-button';
@ -10,7 +8,7 @@ import { Dropdown } from '../base/dropdown/dropdown';
import { DropdownButton } from '../base/dropdown/dropdown-button';
import { DropdownItem } from '../base/dropdown/dropdown-item';
import { Link } from '../base/link';
import { type ModalHandle, Modal } from '../base/modal';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
@ -30,95 +28,53 @@ const TO_ADD_CONTENT_LENGTH: Record<string, string[]> = {
node: ['native'],
};
interface Props {
type Props = ModalProps & {
environmentId: string;
};
export interface GenerateCodeModalOptions {
request?: Request;
}
interface State {
export interface State {
cmd: string;
request?: Request;
target: HTTPSnippetTarget;
client: HTTPSnippetClient;
}
export interface GenerateCodeModalHandle {
show: (options: GenerateCodeModalOptions) => void;
hide: () => void;
}
export const GenerateCodeModal = forwardRef<GenerateCodeModalHandle, Props>((props, ref) => {
const modalRef = useRef<ModalHandle>(null);
const editorRef = useRef<UnconnectedCodeEditor>(null);
@autoBindMethodsForReact(AUTOBIND_CFG)
export class GenerateCodeModal extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
let target: HTTPSnippetTarget | undefined;
let client: HTTPSnippetClient | undefined;
let storedTarget: HTTPSnippetTarget | undefined;
let storedClient: HTTPSnippetClient | undefined;
try {
storedTarget = JSON.parse(window.localStorage.getItem('insomnia::generateCode::target') || '') as HTTPSnippetTarget;
} catch (error) {}
// Load preferences from localStorage
try {
target = JSON.parse(window.localStorage.getItem('insomnia::generateCode::target') || '') as HTTPSnippetTarget;
} catch (error) {}
try {
storedClient = JSON.parse(window.localStorage.getItem('insomnia::generateCode::client') || '') as HTTPSnippetClient;
} catch (error) {}
const [state, setState] = useState<State>({
cmd: '',
request: undefined,
target: storedTarget || DEFAULT_TARGET,
client: storedClient || DEFAULT_CLIENT,
});
try {
client = JSON.parse(window.localStorage.getItem('insomnia::generateCode::client') || '') as HTTPSnippetClient;
} catch (error) {}
this.state = {
cmd: '',
request: undefined,
target: target || DEFAULT_TARGET,
client: client || DEFAULT_CLIENT,
};
}
modal: ModalHandle | null = null;
_editor: UnconnectedCodeEditor | null = null;
_setModalRef(modal: ModalHandle) {
this.modal = modal;
}
_setEditorRef(editor: UnconnectedCodeEditor) {
this._editor = editor;
}
hide() {
this.modal?.hide();
}
_handleClientChange(client: HTTPSnippetClient) {
const { target, request } = this.state;
if (!request) {
return;
}
this._generateCode(request, target, client);
}
_handleTargetChange(target: HTTPSnippetTarget) {
const { target: currentTarget, request } = this.state;
if (currentTarget.key === target.key) {
// No change
return;
}
const client = target.clients.find(c => c.key === target.default);
if (!request) {
return;
}
// TODO: remove non-null assertion
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this._generateCode(request, target, client!);
}
async _generateCode(request: Request, target: HTTPSnippetTarget, client: HTTPSnippetClient) {
const generateCode = useCallback(async (request: Request, target: HTTPSnippetTarget, client: HTTPSnippetClient) => {
// Some clients need a content-length for the request to succeed
const addContentLength = Boolean((TO_ADD_CONTENT_LENGTH[target.key] || []).find(c => c === client.key));
const { environmentId } = this.props;
const har = await exportHarRequest(request._id, environmentId, addContentLength);
const har = await exportHarRequest(request._id, props.environmentId, addContentLength);
// @TODO Should we throw instead?
if (!har) {
return;
}
const snippet = new HTTPSnippet(har);
const cmd = snippet.convert(target.key, client.key) || '';
this.setState({
setState({
request,
cmd,
client,
@ -127,88 +83,96 @@ export class GenerateCodeModal extends PureComponent<Props, State> {
// Save client/target for next time
window.localStorage.setItem('insomnia::generateCode::client', JSON.stringify(client));
window.localStorage.setItem('insomnia::generateCode::target', JSON.stringify(target));
}, [props.environmentId]);
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: options => {
if (!options.request) {
return;
}
generateCode(options.request, state.target, state.client);
modalRef.current?.show();
},
}), [generateCode, state]);
const { cmd, target, client, request } = state;
const targets = HTTPSnippet.availableTargets();
// NOTE: Just some extra precautions in case the target is messed up
let clients: HTTPSnippetClient[] = [];
if (target && Array.isArray(target.clients)) {
clients = target.clients;
}
show(request: Request) {
const { client, target } = this.state;
this._generateCode(request, target, client);
this.modal?.show();
}
render() {
const { cmd, target, client } = this.state;
const targets = HTTPSnippet.availableTargets();
// NOTE: Just some extra precautions in case the target is messed up
let clients: HTTPSnippetClient[] = [];
if (target && Array.isArray(target.clients)) {
clients = target.clients;
}
return (
<Modal ref={this._setModalRef} tall {...this.props}>
<ModalHeader>Generate Client Code</ModalHeader>
<ModalBody
noScroll
style={{
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr)',
gridTemplateRows: 'auto minmax(0, 1fr)',
}}
>
<div className="pad">
<Dropdown outline>
<DropdownButton className="btn btn--clicky">
{
target ? target.title : 'n/a'
}
<i className="fa fa-caret-down" />
</DropdownButton>
{targets.map(target => (
<DropdownItem key={target.key} onClick={() => this._handleTargetChange(target)}>
{target.title}
</DropdownItem>
))}
</Dropdown>
return (
<Modal ref={modalRef} tall {...props}>
<ModalHeader>Generate Client Code</ModalHeader>
<ModalBody
noScroll
style={{
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr)',
gridTemplateRows: 'auto minmax(0, 1fr)',
}}
>
<div className="pad">
<Dropdown outline>
<DropdownButton className="btn btn--clicky">
{target ? target.title : 'n/a'}
<i className="fa fa-caret-down" />
</DropdownButton>
{targets.map(target => (
<DropdownItem
key={target.key}
onClick={() => {
const client = target.clients.find(c => c.key === target.default);
if (request && client) {
generateCode(request, target, client);
}
}}
>
{target.title}
</DropdownItem>
))}
</Dropdown>
&nbsp;&nbsp;
<Dropdown outline>
<DropdownButton className="btn btn--clicky">
{client ? client.title : 'n/a'}
<i className="fa fa-caret-down" />
</DropdownButton>
{clients.map(client => (
<DropdownItem
key={client.key}
onClick={() => this._handleClientChange(client)}
>
{client.title}
</DropdownItem>
))}
</Dropdown>
<Dropdown outline>
<DropdownButton className="btn btn--clicky">
{client ? client.title : 'n/a'}
<i className="fa fa-caret-down" />
</DropdownButton>
{clients.map(client => (
<DropdownItem
key={client.key}
onClick={() => request && generateCode(request, state.target, client)}
>
{client.title}
</DropdownItem>
))}
</Dropdown>
&nbsp;&nbsp;
<CopyButton content={cmd} className="pull-right" />
</div>
<CodeEditor
placeholder="Generating code snippet..."
className="border-top"
key={Date.now()}
mode={MODE_MAP[target.key] || target.key}
ref={this._setEditorRef}
defaultValue={cmd}
/>
</ModalBody>
<ModalFooter>
<div className="margin-left italic txt-sm">
* Code snippets generated by&nbsp;
<Link href="https://github.com/Kong/httpsnippet">httpsnippet</Link>
</div>
<button className="btn" onClick={this.hide}>
Done
</button>
</ModalFooter>
</Modal>
);
}
}
<CopyButton content={cmd} className="pull-right" />
</div>
<CodeEditor
placeholder="Generating code snippet..."
className="border-top"
key={Date.now()}
mode={MODE_MAP[target.key] || target.key}
ref={editorRef}
defaultValue={cmd}
/>
</ModalBody>
<ModalFooter>
<div className="margin-left italic txt-sm">
* Code snippets generated by&nbsp;
<Link href="https://github.com/Kong/httpsnippet">httpsnippet</Link>
</div>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Done
</button>
</ModalFooter>
</Modal>
);
});
GenerateCodeModal.displayName = 'GenerateCodeModal';

View File

@ -1,15 +1,13 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent } from 'react';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import { parseApiSpec } from '../../../common/api-specs';
import { AUTOBIND_CFG } from '../../../common/constants';
import type { ApiSpec } from '../../../models/api-spec';
import type { ConfigGenerator } from '../../../plugins';
import * as plugins from '../../../plugins';
import { CopyButton } from '../base/copy-button';
import { Link } from '../base/link';
import { type ModalHandle, Modal } from '../base/modal';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
@ -30,133 +28,117 @@ interface State {
activeTab: number;
}
interface ShowOptions {
interface GenerateConfigModalOptions {
apiSpec: ApiSpec;
activeTabLabel: string;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
export class GenerateConfigModal extends PureComponent<{}, State> {
modal: ModalHandle | null = null;
state: State = {
export interface GenerateConfigModalHandle {
show: (options: GenerateConfigModalOptions) => void;
hide: () => void;
}
export const GenerateConfigModal = forwardRef<GenerateConfigModalHandle, ModalProps>((_, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<State>({
configs: [],
activeTab: 0,
};
});
_setModalRef(modal: ModalHandle) {
this.modal = modal;
}
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: async options => {
const configs: Config[] = [];
for (const p of await plugins.getConfigGenerators()) {
configs.push(await generateConfig(p, options.apiSpec));
}
const foundIndex = configs.findIndex(c => c.label === options.activeTabLabel);
setState({
configs,
activeTab: foundIndex < 0 ? 0 : foundIndex,
});
modalRef.current?.show();
},
}), []);
async _generate(generatePlugin: ConfigGenerator, apiSpec: ApiSpec) {
const config: Config = {
content: '',
mimeType: 'text/yaml',
label: generatePlugin.label,
docsLink: generatePlugin.docsLink,
error: null,
};
const generateConfig = async (generatePlugin: ConfigGenerator, apiSpec: ApiSpec): Promise<Config> => {
try {
const result = await generatePlugin.generate(parseApiSpec(apiSpec.contents));
if (result.document) {
config.content = result.document;
}
config.error = result.error || null;
return config;
return {
content: result.document || '',
mimeType: 'text/yaml',
label: generatePlugin.label,
docsLink: generatePlugin.docsLink,
error: result.error || null,
};
} catch (err) {
config.error = err.message;
return config;
return {
content: '',
mimeType: 'text/yaml',
label: generatePlugin.label,
docsLink: generatePlugin.docsLink,
error: err.message,
};
}
}
};
async show({ activeTabLabel, apiSpec }: ShowOptions) {
const configs: Config[] = [];
for (const p of await plugins.getConfigGenerators()) {
configs.push(await this._generate(p, apiSpec));
}
const foundIndex = configs.findIndex(c => c.label === activeTabLabel);
this.setState({
const onSelect = (index: number) => {
setState({
configs,
activeTab: foundIndex < 0 ? 0 : foundIndex,
});
this.modal?.show();
}
renderConfigTabPanel(config: Config) {
const linkIcon = <i className="fa fa-external-link-square" />;
if (config.error) {
return (
<TabPanel key={config.label}>
<p className="notice error margin-md">
{config.error}
{config.docsLink ? <><br /><Link href={config.docsLink}>Documentation {linkIcon}</Link></> : null}
</p>
</TabPanel>
);
}
return (
<TabPanel key={config.label}>
<CodeEditor
className="tall pad-top-sm"
defaultValue={config.content}
mode={config.mimeType}
readOnly
/>
</TabPanel>
);
}
_handleTabSelect(index: number) {
this.setState({
activeTab: index,
});
}
};
const { configs, activeTab } = state;
const activeConfig = configs[activeTab];
return (
<Modal ref={modalRef} tall>
<ModalHeader>Generate Config</ModalHeader>
<ModalBody className="wide">
<Tabs forceRenderTabPanel defaultIndex={activeTab} onSelect={onSelect}>
<TabList>{configs.map(config =>
(<Tab key={config.label} tabIndex="-1">
<button>
{config.label}
{config.docsLink ?
<>
{' '}
<HelpTooltip>
To learn more about {config.label}
<br />
<Link href={config.docsLink}>Documentation {<i className="fa fa-external-link-square" />}</Link>
</HelpTooltip>
</> : null}
</button>
</Tab>)
)}
</TabList>
{configs.map(config =>
(<TabPanel key={config.label}>
{config.error ?
<p className="notice error margin-md">
{config.error}
{config.docsLink ? <><br /><Link href={config.docsLink}>Documentation {<i className="fa fa-external-link-square" />}</Link></> : null}
</p> :
<CodeEditor
className="tall pad-top-sm"
defaultValue={config.content}
mode={config.mimeType}
readOnly
/>}
</TabPanel>)
)}
</Tabs>
</ModalBody>
{activeConfig && (
<ModalFooter>
<CopyButton className="btn" content={activeConfig.content}>
Copy to Clipboard
</CopyButton>
</ModalFooter>
)}
</Modal>
);
});
GenerateConfigModal.displayName = 'GenerateConfigModal';
renderConfigTab(config: Config) {
const linkIcon = <i className="fa fa-external-link-square" />;
return (
<Tab key={config.label} tabIndex="-1">
<button>
{config.label}
{config.docsLink ?
<>
{' '}
<HelpTooltip>
To learn more about {config.label}
<br />
<Link href={config.docsLink}>Documentation {linkIcon}</Link>
</HelpTooltip>
</> : null}
</button>
</Tab>
);
}
render() {
const { configs, activeTab } = this.state;
const activeConfig = configs[activeTab];
return (
<Modal ref={this._setModalRef} tall>
<ModalHeader>Generate Config</ModalHeader>
<ModalBody className="wide">
<Tabs forceRenderTabPanel defaultIndex={activeTab} onSelect={this._handleTabSelect}>
<TabList>{configs.map(this.renderConfigTab)}</TabList>
{configs.map(this.renderConfigTabPanel)}
</Tabs>
</ModalBody>
{activeConfig && (
<ModalFooter>
<CopyButton className="btn" content={activeConfig.content}>
Copy to Clipboard
</CopyButton>
</ModalFooter>
)}
</Modal>
);
}
}
export const showGenerateConfigModal = (opts: ShowOptions) => showModal(GenerateConfigModal, opts);
export const showGenerateConfigModal = (opts: GenerateConfigModalOptions) => showModal(GenerateConfigModal, opts);

View File

@ -1,9 +1,7 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent } from 'react';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import { Workspace } from '../../../models/workspace';
import { type ModalHandle, Modal } from '../base/modal';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
@ -14,82 +12,80 @@ interface Props {
workspace: Workspace;
}
interface State {
defaultTemplate: string;
interface NunjucksModalOptions {
template: string;
onDone: Function;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
export class NunjucksModal extends PureComponent<Props, State> {
state: State = {
defaultTemplate: '',
export interface NunjucksModalHandle {
show: (options: NunjucksModalOptions) => void;
hide: () => void;
}
export const NunjucksModal = forwardRef<NunjucksModalHandle, ModalProps & Props>((props, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<NunjucksModalOptions>({
template: '',
onDone: () => { },
});
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: ({ onDone, template }) => {
setState({
template,
onDone,
});
modalRef.current?.show();
},
}), []);
const handleTemplateChange = (template: string) => {
setState(state => ({
template,
onDone: state.onDone,
}));
};
_onDone: Function | null = null;
_currentTemplate: string | null = null;
modal: ModalHandle | null = null;
const { workspace } = props;
const { template } = state;
let editor: JSX.Element | null = null;
let title = '';
_setModalRef(modal: ModalHandle) {
this.modal = modal;
if (template.indexOf('{{') === 0) {
title = 'Variable';
editor = <VariableEditor onChange={handleTemplateChange} defaultValue={template} />;
} else if (template.indexOf('{%') === 0) {
title = 'Tag';
editor = <TagEditor onChange={handleTemplateChange} defaultValue={template} workspace={workspace} />;
}
_handleTemplateChange(template: string | null) {
this._currentTemplate = template;
}
_handleSubmit(event: React.FormEvent) {
event.preventDefault();
this.hide();
}
_handleModalHide() {
if (this._onDone) {
this._onDone(this._currentTemplate);
this.setState({
defaultTemplate: '',
});
}
}
show({ template, onDone }: any) {
this._onDone = onDone;
this._currentTemplate = template;
this.setState({
defaultTemplate: template,
});
this.modal?.show();
}
hide() {
this.modal?.hide();
}
render() {
const { workspace } = this.props;
const { defaultTemplate } = this.state;
let editor: JSX.Element | null = null;
let title = '';
if (defaultTemplate.indexOf('{{') === 0) {
title = 'Variable';
editor = <VariableEditor onChange={this._handleTemplateChange} defaultValue={defaultTemplate} />;
} else if (defaultTemplate.indexOf('{%') === 0) {
title = 'Tag';
editor = <TagEditor onChange={this._handleTemplateChange} defaultValue={defaultTemplate} workspace={workspace} />;
}
return (
<Modal ref={this._setModalRef} onHide={this._handleModalHide}>
<ModalHeader>Edit {title}</ModalHeader>
<ModalBody className="pad" key={defaultTemplate}>
<form onSubmit={this._handleSubmit}>{editor}</form>
</ModalBody>
<ModalFooter>
<button className="btn" onClick={this.hide}>
Done
</button>
</ModalFooter>
</Modal>
);
}
}
return (
<Modal
ref={modalRef}
onHide={() => {
state.onDone(state.template);
setState(state => ({
template: '',
onDone: state.onDone,
}));
}}
>
<ModalHeader>Edit {title}</ModalHeader>
<ModalBody className="pad">
<form
onSubmit={event => {
event.preventDefault();
modalRef.current?.hide();
}}
>{editor}</form>
</ModalBody>
<ModalFooter>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Done
</button>
</ModalFooter>
</Modal>
);
});
NunjucksModal.displayName = 'NunjucksModal';

View File

@ -1,100 +1,88 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { JSONPath } from 'jsonpath-plus';
import React, { PureComponent } from 'react';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import { docsTemplateTags } from '../../../common/documentation';
import { Request } from '../../../models/request';
import { isRequest } from '../../../models/request';
import { RenderError } from '../../../templating';
import { Link } from '../base/link';
import { type ModalHandle, Modal } from '../base/modal';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalHeader } from '../base/modal-header';
import { RequestSettingsModal } from '../modals/request-settings-modal';
import { showModal } from './index';
interface State {
error: Error | null;
request: any | null;
export interface RequestRenderErrorModalOptions {
error: RenderError | null;
request: Request | null;
}
export interface RequestRenderErrorModalHandle {
show: (options: RequestRenderErrorModalOptions) => void;
hide: () => void;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
export class RequestRenderErrorModal extends PureComponent<{}, State> {
state: State = {
export const RequestRenderErrorModal = forwardRef<RequestRenderErrorModalHandle, ModalProps>((_, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<RequestRenderErrorModalOptions>({
error: null,
request: null,
};
});
modal: ModalHandle | null = null;
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: options => {
setState(options);
modalRef.current?.show();
},
}), []);
_setModalRef(modal: ModalHandle) {
this.modal = modal;
}
_handleShowRequestSettings() {
this.hide();
showModal(RequestSettingsModal, {
request: this.state.request,
});
}
show({ request, error }: Pick<State, 'request' | 'error'>) {
this.setState({ request, error });
this.modal?.show();
}
hide() {
this.modal?.hide();
}
renderModalBody(request: any, error: any) {
const fullPath = `Request.${error.path}`;
const result = JSONPath({ json: request, path: `$.${error.path}` });
const template = result && result.length ? result[0] : null;
const locationLabel =
template?.includes('\n') ? `line ${error.location.line} of` : null;
return (
<div className="pad">
<div className="notice warning">
<p>
Failed to render <strong>{fullPath}</strong> prior to sending
</p>
<div className="pad-top-sm">
{error.path.match(/^body/) && isRequest(request) && (
<button
className="btn btn--clicky margin-right-sm"
onClick={this._handleShowRequestSettings}
>
Adjust Render Settings
</button>
)}
<Link button href={docsTemplateTags} className="btn btn--clicky">
Templating Documentation <i className="fa fa-external-link" />
</Link>
const { request, error } = state;
const fullPath = `Request.${error?.path}`;
const result = JSONPath({ json: request, path: `$.${error?.path}` });
const template = result && result.length ? result[0] : null;
const locationLabel = template?.includes('\n') ? `line ${error?.location.line} of` : null;
return (
<Modal ref={modalRef}>
<ModalHeader>Failed to Render Request</ModalHeader>
<ModalBody>{request && error && error ? (
<div className="pad">
<div className="notice warning">
<p>
Failed to render <strong>{fullPath}</strong> prior to sending
</p>
<div className="pad-top-sm">
{error.path?.match(/^body/) && isRequest(request) && (
<button
className="btn btn--clicky margin-right-sm"
onClick={() => {
modalRef.current?.hide();
showModal(RequestSettingsModal, { request: state.request });
}}
>
Adjust Render Settings
</button>
)}
<Link button href={docsTemplateTags} className="btn btn--clicky">
Templating Documentation <i className="fa fa-external-link" />
</Link>
</div>
</div>
<p>
<strong>Render error</strong>
<code className="block selectable">{error.message}</code>
</p>
<p>
<strong>Caused by the following field</strong>
<code className="block">
{locationLabel} {fullPath}
</code>
</p>
</div>
<p>
<strong>Render error</strong>
<code className="block selectable">{error.message}</code>
</p>
<p>
<strong>Caused by the following field</strong>
<code className="block">
{locationLabel} {fullPath}
</code>
</p>
</div>
);
}
render() {
const { request, error } = this.state;
return (
<Modal ref={this._setModalRef}>
<ModalHeader>Failed to Render Request</ModalHeader>
<ModalBody>{request && error ? this.renderModalBody(request, error) : null}</ModalBody>
</Modal>
);
}
}
) : null}</ModalBody>
</Modal>
);
});
RequestRenderErrorModal.displayName = 'RequestRenderErrorModal';

View File

@ -1,14 +1,12 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { createRef, PureComponent } from 'react';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import { type ModalHandle, Modal } from '../base/modal';
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
import { showModal } from '.';
export interface SelectModalShowOptions {
export interface SelectModalOptions {
message: string | null;
onDone?: (selectedValue: string | null) => void | Promise<void>;
options: {
@ -19,77 +17,60 @@ export interface SelectModalShowOptions {
value: string | null;
noEscape?: boolean;
}
const initialState: SelectModalShowOptions = {
message: null,
options: [],
title: null,
value: null,
};
@autoBindMethodsForReact(AUTOBIND_CFG)
export class SelectModal extends PureComponent<{}, SelectModalShowOptions> {
modal = createRef<ModalHandle>();
doneButton = createRef<HTMLButtonElement>();
state: SelectModalShowOptions = initialState;
async _onDone() {
this.modal.current?.hide();
await this.state.onDone?.(this.state.value);
}
_handleSelectChange(event: React.SyntheticEvent<HTMLSelectElement>) {
this.setState({ value: event.currentTarget.value });
}
show({
message,
onDone,
options,
title,
value,
noEscape,
}: SelectModalShowOptions = initialState) {
this.setState({
message,
onDone,
options,
title,
value,
noEscape,
});
this.modal.current?.show();
setTimeout(() => {
this.doneButton.current?.focus();
}, 100);
}
render() {
const { message, title, options, value, noEscape } = this.state;
return (
<Modal ref={this.modal} noEscape={noEscape}>
<ModalHeader>{title || 'Confirm?'}</ModalHeader>
<ModalBody className="wide pad">
<p>{message}</p>
<div className="form-control form-control--outlined">
<select onChange={this._handleSelectChange} value={value ?? undefined}>
{options.map(({ name, value }) => (
<option key={value} value={value}>
{name}
</option>
))}
</select>
</div>
</ModalBody>
<ModalFooter>
<button ref={this.doneButton} className="btn" onClick={this._onDone}>
Done
</button>
</ModalFooter>
</Modal>
);
}
export interface SelectModalHandle {
show: (options: SelectModalOptions) => void;
hide: () => void;
}
export const showSelectModal = (opts: SelectModalShowOptions) => showModal(SelectModal, opts);
export const SelectModal = forwardRef<SelectModalHandle, ModalProps>((_, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<SelectModalOptions>({
message: null,
options: [],
title: null,
value: null,
});
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: options => {
setState(options);
modalRef.current?.show();
},
}), []);
const { message, title, options, value, noEscape, onDone } = state;
return (
<Modal ref={modalRef} noEscape={noEscape}>
<ModalHeader>{title || 'Confirm?'}</ModalHeader>
<ModalBody className="wide pad">
<p>{message}</p>
<div className="form-control form-control--outlined">
<select onChange={event => setState(state => ({ ...state, value: event.target.value }))} value={value ?? undefined}>
{options.map(({ name, value }) => (
<option key={value} value={value}>
{name}
</option>
))}
</select>
</div>
</ModalBody>
<ModalFooter>
<button
className="btn"
onClick={() => {
modalRef.current?.hide();
onDone?.(value);
}}
>
Done
</button>
</ModalFooter>
</Modal>
);
});
SelectModal.displayName = 'SelectModal';
export const showSelectModal = (opts: SelectModalOptions) => showModal(SelectModal, opts);

View File

@ -1,70 +1,48 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent, ReactNode } from 'react';
import React, { forwardRef, ReactNode, useImperativeHandle, useRef, useState } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import { type ModalHandle, Modal } from '../base/modal';
import { type ModalProps, Modal, ModalHandle } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalHeader } from '../base/modal-header';
interface State {
interface WrapperModalOptions {
title: string;
body: ReactNode;
bodyHTML?: string | null;
tall?: boolean | null;
skinny?: boolean | null;
wide?: boolean | null;
tall?: boolean;
skinny?: boolean;
wide?: boolean;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
export class WrapperModal extends PureComponent<{}, State> {
modal: ModalHandle | null = null;
state: State = {
export interface WrapperModalHandle {
show: (options: WrapperModalOptions) => void;
hide: () => void;
}
export const WrapperModal = forwardRef<WrapperModalHandle, ModalProps>((props, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<WrapperModalOptions>({
title: '',
body: null,
bodyHTML: null,
tall: false,
skinny: false,
wide: false,
};
});
_setModalRef(modal: ModalHandle) {
this.modal = modal;
}
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: options => {
setState(options);
modalRef.current?.show();
},
}), []);
show(options: Record<string, any> = {}) {
const { title, body, bodyHTML, tall, skinny, wide } = options;
this.setState({
title,
body,
bodyHTML,
tall: !!tall,
skinny: !!skinny,
wide: !!wide,
});
this.modal?.show();
}
const { title, body, tall, skinny, wide } = state;
render() {
const { title, body, bodyHTML, tall, skinny, wide } = this.state;
let finalBody = body;
return (
<Modal ref={modalRef} tall={tall} skinny={skinny} wide={wide} {...props}>
<ModalHeader>{title || 'Uh Oh!'}</ModalHeader>
<ModalBody>{body}</ModalBody>
</Modal>
);
if (bodyHTML) {
finalBody = (
<div
dangerouslySetInnerHTML={{
__html: bodyHTML,
}}
className="tall wide pad"
/>
);
}
return (
<Modal ref={this._setModalRef} tall={tall ?? undefined} skinny={skinny ?? undefined} wide={wide ?? undefined}>
<ModalHeader>{title || 'Uh Oh!'}</ModalHeader>
<ModalBody>{finalBody}</ModalBody>
</Modal>
);
}
}
});
WrapperModal.displayName = 'WrapperModal';

View File

@ -61,7 +61,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
const buttonRef = useRef<HTMLButtonElement>(null);
const handleGenerateCode = () => {
showModal(GenerateCodeModal, request);
showModal(GenerateCodeModal, { request });
};
const focusInput = useCallback(() => {
if (inputRef.current) {

View File

@ -144,7 +144,7 @@ export const WrapperDebug: FC = () => {
showCookiesEditor:
() => showModal(CookiesModal),
request_showGenerateCodeEditor:
() => showModal(GenerateCodeModal, activeRequest),
() => showModal(GenerateCodeModal, { request: activeRequest }),
});
// Close all websocket connections when the active environment changes
useEffect(() => {

View File

@ -212,19 +212,19 @@ const App = () => {
<div key="modals" className="modals">
<ErrorBoundary showAlert>
<AnalyticsModal />
<AlertModal ref={registerModal} />
<ErrorModal ref={registerModal} />
<AlertModal ref={instance => registerModal(instance, 'AlertModal')} />
<ErrorModal ref={instance => registerModal(instance, 'ErrorModal')} />
<PromptModal ref={registerModal} />
<WrapperModal ref={registerModal} />
<WrapperModal ref={instance => registerModal(instance, 'WrapperModal')} />
<LoginModal ref={registerModal} />
<AskModal ref={registerModal} />
<SelectModal ref={registerModal} />
<FilterHelpModal ref={registerModal} />
<RequestRenderErrorModal ref={registerModal} />
<GenerateConfigModal ref={registerModal} />
<AskModal ref={instance => registerModal(instance, 'AskModal')} />
<SelectModal ref={instance => registerModal(instance, 'SelectModal')} />
<FilterHelpModal ref={instance => registerModal(instance, 'FilterHelpModal')} />
<RequestRenderErrorModal ref={instance => registerModal(instance, 'RequestRenderErrorModal')} />
<GenerateConfigModal ref={instance => registerModal(instance, 'GenerateConfigModal')} />
<ProjectSettingsModal ref={instance => registerModal(instance, 'ProjectSettingsModal')} />
<WorkspaceDuplicateModal ref={registerModal} vcs={vcs || undefined} />
<CodePromptModal ref={registerModal} />
<CodePromptModal ref={instance => registerModal(instance, 'CodePromptModal')} />
<RequestSettingsModal ref={instance => registerModal(instance, 'RequestSettingsModal')} />
<RequestGroupSettingsModal ref={instance => registerModal(instance, 'RequestGroupSettingsModal')} />
@ -238,7 +238,7 @@ const App = () => {
</> : null}
<NunjucksModal
ref={registerModal}
ref={instance => registerModal(instance, 'NunjucksModal')}
workspace={activeWorkspace}
/>
@ -250,7 +250,7 @@ const App = () => {
</> : null}
<GenerateCodeModal
ref={registerModal}
ref={instance => registerModal(instance, 'GenerateCodeModal')}
environmentId={activeEnvironment ? activeEnvironment._id : 'n/a'}
/>
@ -259,10 +259,7 @@ const App = () => {
<RequestSwitcherModal ref={instance => registerModal(instance, 'RequestSwitcherModal')} />
<EnvironmentEditModal
ref={registerModal}
onChange={models.requestGroup.update}
/>
<EnvironmentEditModal ref={instance => registerModal(instance, 'EnvironmentEditModal')} />
<GitRepositorySettingsModal ref={registerModal} />
@ -297,7 +294,7 @@ const App = () => {
/>
<AddKeyCombinationModal ref={instance => registerModal(instance, 'AddKeyCombinationModal')} />
<ExportRequestsModal ref={registerModal} />
<ExportRequestsModal ref={instance => registerModal(instance, 'ExportRequestsModal')} />
<GrpcDispatchModalWrapper>
{dispatch => (