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; handleGenerateCode: Function; handleImport: Function; handleSend: () => void; handleSendAndDownload: (filepath?: string) => Promise; handleUpdateDownloadPath: Function; nunjucksPowerUserMode: boolean; onMethodChange: (r: Request, method: string) => Promise; onUrlChange: (r: Request, url: string) => Promise; 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 { _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) { e.preventDefault(); this._dropdown?.show(); } _handleFormSubmit(e: React.SyntheticEvent) { 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) { 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 = ( ); } else if (currentTimeout) { cancelButton = ( ); } let sendButton; if (!cancelButton) { sendButton = ( {downloadPath ? 'Download' : 'Send'} Basic Send Now Generate Client Code Advanced Send After Delay Repeat on Interval {downloadPath ? ( Stop Auto-Download ) : ( Download After Send )} Send And Download ); } return [cancelButton, sendButton]; } render() { const { request, handleAutocompleteUrls, uniquenessKey, } = this.props; const { url, method } = request; return (
{method}
{this.renderSendButton()}
); } }