mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 14:49:53 +00:00
384 lines
11 KiB
TypeScript
384 lines
11 KiB
TypeScript
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
|
import { OpenDialogOptions, remote } from 'electron';
|
|
import { HotKeyRegistry } from 'insomnia-common';
|
|
import React, { PureComponent, ReactNode } from 'react';
|
|
|
|
import { AUTOBIND_CFG, DEBOUNCE_MILLIS, isMac } from '../../common/constants';
|
|
import { hotKeyRefs } from '../../common/hotkeys';
|
|
import { executeHotKey } from '../../common/hotkeys-listener';
|
|
import type { Request } from '../../models/request';
|
|
import { Dropdown } from './base/dropdown/dropdown';
|
|
import { DropdownButton } from './base/dropdown/dropdown-button';
|
|
import { DropdownDivider } from './base/dropdown/dropdown-divider';
|
|
import { DropdownHint } from './base/dropdown/dropdown-hint';
|
|
import { DropdownItem } from './base/dropdown/dropdown-item';
|
|
import { PromptButton } from './base/prompt-button';
|
|
import { OneLineEditor } from './codemirror/one-line-editor';
|
|
import { MethodDropdown } from './dropdowns/method-dropdown';
|
|
import { KeydownBinder } from './keydown-binder';
|
|
import { showPrompt } from './modals/index';
|
|
|
|
interface Props {
|
|
handleAutocompleteUrls: () => Promise<string[]>;
|
|
handleGenerateCode: Function;
|
|
handleImport: Function;
|
|
handleSend: () => void;
|
|
handleSendAndDownload: (filepath?: string) => Promise<void>;
|
|
handleUpdateDownloadPath: Function;
|
|
nunjucksPowerUserMode: boolean;
|
|
onMethodChange: (r: Request, method: string) => Promise<Request>;
|
|
onUrlChange: (r: Request, url: string) => Promise<Request>;
|
|
request: Request;
|
|
uniquenessKey: string;
|
|
hotKeyRegistry: HotKeyRegistry;
|
|
downloadPath: string | null;
|
|
}
|
|
|
|
interface State {
|
|
currentInterval: number | null;
|
|
currentTimeout: number | null;
|
|
}
|
|
|
|
@autoBindMethodsForReact(AUTOBIND_CFG)
|
|
export class RequestUrlBar extends PureComponent<Props, State> {
|
|
_urlChangeDebounceTimeout: NodeJS.Timeout | null = null;
|
|
_sendTimeout: NodeJS.Timeout | null = null;
|
|
_sendInterval: NodeJS.Timeout | null = null;
|
|
_dropdown: Dropdown | null = null;
|
|
_methodDropdown: MethodDropdown | null = null;
|
|
_input: OneLineEditor | null = null;
|
|
state: State = {
|
|
currentInterval: null,
|
|
currentTimeout: null,
|
|
};
|
|
|
|
_lastPastedText?: string;
|
|
|
|
_setDropdownRef(n: Dropdown) {
|
|
this._dropdown = n;
|
|
}
|
|
|
|
_setMethodDropdownRef(n: MethodDropdown) {
|
|
this._methodDropdown = n;
|
|
}
|
|
|
|
_setInputRef(n: OneLineEditor) {
|
|
this._input = n;
|
|
}
|
|
|
|
_handleMetaClickSend(e: React.MouseEvent<HTMLButtonElement>) {
|
|
e.preventDefault();
|
|
this._dropdown?.show();
|
|
}
|
|
|
|
_handleFormSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
this._handleSend();
|
|
}
|
|
|
|
_handleMethodChange(method: string) {
|
|
this.props.onMethodChange(this.props.request, method);
|
|
}
|
|
|
|
_handleUrlChange(url: string) {
|
|
if (this._urlChangeDebounceTimeout !== null) {
|
|
clearTimeout(this._urlChangeDebounceTimeout);
|
|
}
|
|
this._urlChangeDebounceTimeout = setTimeout(async () => {
|
|
const pastedText = this._lastPastedText;
|
|
|
|
// If no pasted text in the queue, just fire the regular change handler
|
|
if (!pastedText) {
|
|
this.props.onUrlChange(this.props.request, url);
|
|
return;
|
|
}
|
|
|
|
// Reset pasted text cache
|
|
this._lastPastedText = undefined;
|
|
// Attempt to import the pasted text
|
|
const importedRequest = await this.props.handleImport(pastedText);
|
|
|
|
// Update depending on whether something was imported
|
|
if (!importedRequest) {
|
|
this.props.onUrlChange(this.props.request, url);
|
|
}
|
|
}, DEBOUNCE_MILLIS);
|
|
}
|
|
|
|
_handleUrlPaste(e: ClipboardEvent) {
|
|
// NOTE: We're not actually doing the import here to avoid races with onChange
|
|
this._lastPastedText = e.clipboardData?.getData('text/plain');
|
|
}
|
|
|
|
_handleGenerateCode() {
|
|
this.props.handleGenerateCode();
|
|
}
|
|
|
|
async _handleSetDownloadLocation() {
|
|
const { request } = this.props;
|
|
const options: OpenDialogOptions = {
|
|
title: 'Select Download Location',
|
|
buttonLabel: 'Select',
|
|
properties: ['openDirectory'],
|
|
};
|
|
const { canceled, filePaths } = await remote.dialog.showOpenDialog(options);
|
|
|
|
if (canceled) {
|
|
return;
|
|
}
|
|
|
|
this.props.handleUpdateDownloadPath(request._id, filePaths[0]);
|
|
}
|
|
|
|
_handleClearDownloadLocation() {
|
|
const { request } = this.props;
|
|
this.props.handleUpdateDownloadPath(request._id, null);
|
|
}
|
|
|
|
async _handleKeyDown(event: KeyboardEvent) {
|
|
if (!this._input) {
|
|
return;
|
|
}
|
|
|
|
executeHotKey(event, hotKeyRefs.REQUEST_FOCUS_URL, () => {
|
|
this._input?.focus();
|
|
this._input?.selectAll();
|
|
});
|
|
executeHotKey(event, hotKeyRefs.REQUEST_TOGGLE_HTTP_METHOD_MENU, () => {
|
|
this._methodDropdown?.toggle();
|
|
});
|
|
executeHotKey(event, hotKeyRefs.REQUEST_SHOW_OPTIONS, () => {
|
|
this._dropdown?.toggle(true);
|
|
});
|
|
}
|
|
|
|
_handleSend() {
|
|
// Don't stop interval because duh, it needs to keep going!
|
|
// XXX this._handleStopInterval(); XXX
|
|
this._handleStopTimeout();
|
|
|
|
const { downloadPath } = this.props;
|
|
|
|
if (downloadPath) {
|
|
this.props.handleSendAndDownload(downloadPath);
|
|
} else {
|
|
this.props.handleSend();
|
|
}
|
|
}
|
|
|
|
_handleClickSendAndDownload() {
|
|
this.props.handleSendAndDownload();
|
|
}
|
|
|
|
_handleSendAfterDelay() {
|
|
showPrompt({
|
|
inputType: 'decimal',
|
|
title: 'Send After Delay',
|
|
label: 'Delay in seconds',
|
|
// @ts-expect-error TSCONVERSION string vs number issue
|
|
defaultValue: 3,
|
|
submitName: 'Start',
|
|
onComplete: seconds => {
|
|
this._handleStopTimeout();
|
|
|
|
// @ts-expect-error TSCONVERSION string vs number issue
|
|
this._sendTimeout = setTimeout(this._handleSend, seconds * 1000);
|
|
this.setState({
|
|
// @ts-expect-error TSCONVERSION string vs number issue
|
|
currentTimeout: seconds,
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
_handleSendOnInterval() {
|
|
showPrompt({
|
|
inputType: 'decimal',
|
|
title: 'Send on Interval',
|
|
label: 'Interval in seconds',
|
|
// @ts-expect-error TSCONVERSION string vs number issue
|
|
defaultValue: 3,
|
|
submitName: 'Start',
|
|
onComplete: seconds => {
|
|
this._handleStopInterval();
|
|
|
|
// @ts-expect-error TSCONVERSION string vs number issue
|
|
this._sendInterval = setInterval(this._handleSend, seconds * 1000);
|
|
this.setState({
|
|
// @ts-expect-error TSCONVERSION string vs number issue
|
|
currentInterval: seconds,
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
_handleStopInterval() {
|
|
if (this._sendInterval) {
|
|
clearInterval(this._sendInterval);
|
|
}
|
|
|
|
if (this.state.currentInterval) {
|
|
this.setState({
|
|
currentInterval: null,
|
|
});
|
|
}
|
|
}
|
|
|
|
_handleStopTimeout() {
|
|
if (this._sendTimeout !== null) {
|
|
clearTimeout(this._sendTimeout);
|
|
}
|
|
|
|
if (this.state.currentTimeout) {
|
|
this.setState({
|
|
currentTimeout: null,
|
|
});
|
|
}
|
|
}
|
|
|
|
_handleResetTimeouts() {
|
|
this._handleStopTimeout();
|
|
|
|
this._handleStopInterval();
|
|
}
|
|
|
|
_handleClickSend(e: React.MouseEvent<HTMLButtonElement>) {
|
|
const metaPressed = isMac() ? e.metaKey : e.ctrlKey;
|
|
|
|
// If we're pressing a meta key, let the dropdown open
|
|
if (metaPressed) {
|
|
return;
|
|
}
|
|
|
|
// If we're not pressing a meta key, cancel dropdown and send the request
|
|
e.stopPropagation(); // Don't trigger the dropdown
|
|
|
|
this._handleSend();
|
|
}
|
|
|
|
// eslint-disable-next-line camelcase
|
|
UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
|
if (nextProps.request._id !== this.props.request._id) {
|
|
this._handleResetTimeouts();
|
|
}
|
|
}
|
|
|
|
renderSendButton() {
|
|
const { hotKeyRegistry, downloadPath } = this.props;
|
|
const { currentInterval, currentTimeout } = this.state;
|
|
let cancelButton: ReactNode = null;
|
|
|
|
if (currentInterval) {
|
|
cancelButton = (
|
|
<button
|
|
type="button"
|
|
key="cancel-interval"
|
|
className="urlbar__send-btn danger"
|
|
onClick={this._handleStopInterval}
|
|
>
|
|
Stop
|
|
</button>
|
|
);
|
|
} else if (currentTimeout) {
|
|
cancelButton = (
|
|
<button
|
|
type="button"
|
|
key="cancel-timeout"
|
|
className="urlbar__send-btn danger"
|
|
onClick={this._handleStopTimeout}
|
|
>
|
|
Cancel
|
|
</button>
|
|
);
|
|
}
|
|
|
|
let sendButton;
|
|
|
|
if (!cancelButton) {
|
|
sendButton = (
|
|
<Dropdown key="dropdown" className="tall" right ref={this._setDropdownRef}>
|
|
<DropdownButton
|
|
className="urlbar__send-btn"
|
|
onContextMenu={this._handleMetaClickSend}
|
|
onClick={this._handleClickSend}
|
|
>
|
|
{downloadPath ? 'Download' : 'Send'}
|
|
</DropdownButton>
|
|
<DropdownDivider>Basic</DropdownDivider>
|
|
<DropdownItem onClick={this._handleClickSend}>
|
|
<i className="fa fa-arrow-circle-o-right" /> Send Now
|
|
<DropdownHint keyBindings={hotKeyRegistry[hotKeyRefs.REQUEST_SEND.id]} />
|
|
</DropdownItem>
|
|
<DropdownItem onClick={this._handleGenerateCode}>
|
|
<i className="fa fa-code" /> Generate Client Code
|
|
</DropdownItem>
|
|
<DropdownDivider>Advanced</DropdownDivider>
|
|
<DropdownItem onClick={this._handleSendAfterDelay}>
|
|
<i className="fa fa-clock-o" /> Send After Delay
|
|
</DropdownItem>
|
|
<DropdownItem onClick={this._handleSendOnInterval}>
|
|
<i className="fa fa-repeat" /> Repeat on Interval
|
|
</DropdownItem>
|
|
{downloadPath ? (
|
|
<DropdownItem
|
|
stayOpenAfterClick
|
|
addIcon
|
|
buttonClass={PromptButton}
|
|
onClick={this._handleClearDownloadLocation}
|
|
>
|
|
<i className="fa fa-stop-circle" /> Stop Auto-Download
|
|
</DropdownItem>
|
|
) : (
|
|
<DropdownItem onClick={this._handleSetDownloadLocation}>
|
|
<i className="fa fa-download" /> Download After Send
|
|
</DropdownItem>
|
|
)}
|
|
<DropdownItem onClick={this._handleClickSendAndDownload}>
|
|
<i className="fa fa-download" /> Send And Download
|
|
</DropdownItem>
|
|
</Dropdown>
|
|
);
|
|
}
|
|
|
|
return [cancelButton, sendButton];
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
request,
|
|
handleAutocompleteUrls,
|
|
uniquenessKey,
|
|
} = this.props;
|
|
const { url, method } = request;
|
|
return (
|
|
<KeydownBinder onKeydown={this._handleKeyDown}>
|
|
<div className="urlbar">
|
|
<MethodDropdown
|
|
ref={this._setMethodDropdownRef}
|
|
onChange={this._handleMethodChange}
|
|
method={method}
|
|
>
|
|
{method} <i className="fa fa-caret-down" />
|
|
</MethodDropdown>
|
|
<form onSubmit={this._handleFormSubmit}>
|
|
<OneLineEditor
|
|
key={uniquenessKey}
|
|
ref={this._setInputRef}
|
|
onPaste={this._handleUrlPaste}
|
|
forceEditor
|
|
type="text"
|
|
getAutocompleteConstants={handleAutocompleteUrls}
|
|
placeholder="https://api.myproduct.com/v1/users"
|
|
defaultValue={url}
|
|
onChange={this._handleUrlChange}
|
|
/>
|
|
{this.renderSendButton()}
|
|
</form>
|
|
</div>
|
|
</KeydownBinder>
|
|
);
|
|
}
|
|
}
|