From 381cf74a19918ffa7e54908e458b0d2939ca023b Mon Sep 17 00:00:00 2001 From: James Gatz Date: Wed, 8 Sep 2021 17:38:07 +0200 Subject: [PATCH] Improve codemirror typings (#3993) Co-authored-by: Dimitri Mitropoulos --- .eslintrc.js | 1 + .../insomnia-app/app/codemirror-graphql.d.ts | 7 + packages/insomnia-app/app/codemirror.d.ts | 75 ++++- packages/insomnia-app/app/httpsnippet.d.ts | 33 +++ packages/insomnia-app/app/models/settings.ts | 1 + .../ui/components/codemirror/code-editor.tsx | 183 ++++++------ .../codemirror/extensions/autocomplete.ts | 262 +++++++++--------- .../components/codemirror/modes/nunjucks.ts | 7 + .../components/codemirror/one-line-editor.tsx | 33 ++- .../components/editors/auth/o-auth-1-auth.tsx | 2 +- .../components/editors/auth/o-auth-2-auth.tsx | 2 +- .../editors/body/graph-ql-editor.tsx | 161 ++++++----- .../graph-ql-explorer/graph-ql-explorer.tsx | 93 +++++-- .../graph-ql-explorer/graph-ql-types.ts | 9 +- .../ui/components/key-value-editor/row.tsx | 5 +- .../app/ui/components/markdown-editor.tsx | 4 +- .../components/modals/code-prompt-modal.tsx | 12 +- .../components/modals/generate-code-modal.tsx | 64 ++--- .../modals/generate-config-modal.tsx | 2 - .../app/ui/components/request-url-bar.tsx | 10 +- .../app/ui/components/wrapper-unit-test.tsx | 7 +- .../insomnia-app/app/ui/containers/app.tsx | 6 +- packages/insomnia-app/package-lock.json | 48 ++-- packages/insomnia-app/package.json | 6 +- 24 files changed, 612 insertions(+), 421 deletions(-) create mode 100644 packages/insomnia-app/app/codemirror-graphql.d.ts create mode 100644 packages/insomnia-app/app/httpsnippet.d.ts diff --git a/.eslintrc.js b/.eslintrc.js index 6162de0f50..0f53327027 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -141,6 +141,7 @@ module.exports = { '@typescript-eslint/keyword-spacing': ERROR, '@typescript-eslint/member-delimiter-style': ERROR, '@typescript-eslint/no-empty-function': OFF(UNKNOWN), + '@typescript-eslint/no-empty-interface': [ERROR, { 'allowSingleExtends': true }], '@typescript-eslint/no-namespace': [ERROR, { allowDeclarations: true }], '@typescript-eslint/no-redeclare': ERROR, '@typescript-eslint/no-unused-vars': [ERROR, { ignoreRestSiblings: true }], diff --git a/packages/insomnia-app/app/codemirror-graphql.d.ts b/packages/insomnia-app/app/codemirror-graphql.d.ts new file mode 100644 index 0000000000..3e2ec1ddcf --- /dev/null +++ b/packages/insomnia-app/app/codemirror-graphql.d.ts @@ -0,0 +1,7 @@ +import { GraphQLInfoOptions } from 'codemirror-graphql/info'; + +declare module 'codemirror-graphql/jump' { + type ModifiedGraphQLJumpOptions = Omit & { + onClick: GraphQLInfoOptions['onClick']; + }; +} diff --git a/packages/insomnia-app/app/codemirror.d.ts b/packages/insomnia-app/app/codemirror.d.ts index d4f77ffc2a..0d76353870 100644 --- a/packages/insomnia-app/app/codemirror.d.ts +++ b/packages/insomnia-app/app/codemirror.d.ts @@ -1,6 +1,11 @@ import 'codemirror'; +import { GraphQLInfoOptions } from 'codemirror-graphql/info'; +import { ModifiedGraphQLJumpOptions } from 'codemirror-graphql/jump'; +import { GraphQLSchema } from 'graphql'; + import { HandleGetRenderContext, HandleRender } from './common/render'; +import { NunjucksParsedTag } from './templating/utils'; type LinkClickCallback = (url: string) => void; @@ -18,8 +23,74 @@ interface InsomniaExtensions { declare module 'codemirror' { type CodeMirrorLinkClickCallback = LinkClickCallback; - /* eslint-disable @typescript-eslint/no-empty-interface */ interface Editor extends InsomniaExtensions {} interface EditorFromTextEditor extends InsomniaExtensions {} - /* eslint-enable @typescript-eslint/no-empty-interface */ + interface TextMarker { + // This flag is being used internally by codemirror and the fold extension + __isFold: boolean; + } + + interface Variable { + name: string; + value: any; + } + + interface Snippet { + name: string; + displayValue: string; + value: string | (() => Promise); + } + + interface EnvironmentAutocompleteOptions { + getConstants?: () => string[] | PromiseLike; + getVariables?: () => Variable[] | PromiseLike; + getSnippets?: () => Snippet[] | PromiseLike; + getTags?: () => NunjucksParsedTag[] | PromiseLike; + } + + interface EditorConfiguration { + info?: GraphQLInfoOptions; + jump?: ModifiedGraphQLJumpOptions; + environmentAutocomplete?: EnvironmentAutocompleteOptions; + } + + interface Hint { + /** + * Custom Insomnia Key. Used for checking the type of the hint + */ + type: 'constant' | 'variable' | 'snippet' | 'tag'; + /** + * Custom Insomnia Key. The segment that matched and produced this hint + */ + segment: string; + /** + * Custom Insomnia Key. This value gets displayed in the autocomplete menu. + */ + displayValue: string; + /** + * Custom Insomnia Key. The display value of the hint + */ + comment?: string; + /** + * Custom Insomnia Key. Used for sorting the hints + */ + score: number; + } + + interface ShowHintOptions { + variables?: Variable[]; + constants?: string[]; + snippets?: Snippet[]; + tags?: NunjucksParsedTag[]; + showAllOnNoMatch?: boolean; + } + + interface LintOptions { + schema?: GraphQLSchema; + } + + interface EditorEventMap { + fold: (instance: Editor, from: Position) => void; + unfold: (instance: Editor, from: Position) => void; + } } diff --git a/packages/insomnia-app/app/httpsnippet.d.ts b/packages/insomnia-app/app/httpsnippet.d.ts new file mode 100644 index 0000000000..929b9cbdae --- /dev/null +++ b/packages/insomnia-app/app/httpsnippet.d.ts @@ -0,0 +1,33 @@ +// @TODO We should make a PR for improving the types of httpsnippet. +declare module 'httpsnippet' { + export interface HTTPSnippetClient { + key: string; + title: string; + link: string; + description: string; + } + + export interface HTTPSnippetTarget { + key: string; + title: string; + extname: string; + default: string; + clients: HTTPSnippetClient[]; + } + + class HTTPSnippet { + constructor(data: any); + convert(target: string, options?: any): string | false; + + convert(target: string, client?: string, options?: any): string | false; + + static addTarget(target: HTTPSnippetTarget): void; + + static addTargetClient(target: string, client: HTTPSnippetClient): void; + + static extName(target: string): string; + static availableTargets(): HTTPSnippetTarget[]; + } + + export default HTTPSnippet; +} diff --git a/packages/insomnia-app/app/models/settings.ts b/packages/insomnia-app/app/models/settings.ts index 67c78d2f61..c4a055a27f 100644 --- a/packages/insomnia-app/app/models/settings.ts +++ b/packages/insomnia-app/app/models/settings.ts @@ -66,6 +66,7 @@ export interface BaseSettings { hasPromptedToMigrateFromDesigner: boolean; hasPromptedOnboarding: boolean; hasPromptedAnalytics: boolean; + isVariableUncovered?: boolean; } export type Settings = BaseModel & BaseSettings; diff --git a/packages/insomnia-app/app/ui/components/codemirror/code-editor.tsx b/packages/insomnia-app/app/ui/components/codemirror/code-editor.tsx index ce4eb47333..da8630d7f0 100644 --- a/packages/insomnia-app/app/ui/components/codemirror/code-editor.tsx +++ b/packages/insomnia-app/app/ui/components/codemirror/code-editor.tsx @@ -3,7 +3,9 @@ import './base-imports'; import { autoBindMethodsForReact } from 'class-autobind-decorator'; import classnames from 'classnames'; import clone from 'clone'; -import CodeMirror, { CodeMirrorLinkClickCallback } from 'codemirror'; +import CodeMirror, { CodeMirrorLinkClickCallback, ShowHintOptions } from 'codemirror'; +import { GraphQLInfoOptions } from 'codemirror-graphql/info'; +import { ModifiedGraphQLJumpOptions } from 'codemirror-graphql/jump'; import deepEqual from 'deep-equal'; import { json as jsonPrettify } from 'insomnia-prettify'; import { query as queryXPath } from 'insomnia-xpath'; @@ -40,9 +42,7 @@ const BASE_CODEMIRROR_OPTIONS: CodeMirror.EditorConfiguration = { lineNumbers: true, placeholder: 'Start Typing...', foldGutter: true, - height: 'auto', - // @ts-expect-error -- TSCONVERSION should be autoRefresh: { delay: 2000 } - autoRefresh: 2000, + autoRefresh: { delay: 2000 }, lineWrapping: true, scrollbarStyle: 'native', lint: true, @@ -89,20 +89,20 @@ export type CodeEditorOnChange = (value: string) => void; interface Props { indentWithTabs?: boolean; onChange?: CodeEditorOnChange; - onCursorActivity?: Function; - onFocus?: Function; - onBlur?: Function; + onCursorActivity?: (cm: CodeMirror.EditorFromTextArea) => void; + onFocus?: (e: FocusEvent) => void; + onBlur?: (e: FocusEvent) => void; onClickLink?: CodeMirrorLinkClickCallback; - onKeyDown?: Function; + onKeyDown?: (e: KeyboardEvent, value: string) => void; onMouseLeave?: React.MouseEventHandler; onClick?: React.MouseEventHandler; - onPaste?: Function; + onPaste?: (e: ClipboardEvent) => void; onCodeMirrorInit?: (editor: CodeMirror.EditorFromTextArea) => void; render?: HandleRender; nunjucksPowerUserMode?: boolean; getRenderContext?: HandleGetRenderContext; - getAutocompleteConstants?: Function; - getAutocompleteSnippets?: Function; + getAutocompleteConstants?: () => string[] | PromiseLike; + getAutocompleteSnippets?: () => CodeMirror.Snippet[]; keyMap?: string; mode?: string; id?: string; @@ -133,10 +133,10 @@ interface Props { debounceMillis?: number; dynamicHeight?: boolean; autoCloseBrackets?: boolean; - hintOptions?: Object; - lintOptions?: Object; - infoOptions?: Object; - jumpOptions?: Object; + hintOptions?: ShowHintOptions; + lintOptions?: any; + infoOptions?: GraphQLInfoOptions; + jumpOptions?: ModifiedGraphQLJumpOptions; uniquenessKey?: string; isVariableUncovered?: boolean; raw?: boolean; @@ -146,6 +146,14 @@ interface State { filter: string; } +function isMarkerRange(mark?: CodeMirror.Position | CodeMirror.MarkerRange): mark is CodeMirror.MarkerRange { + if (!mark) { + return false; + } + + return Object.prototype.hasOwnProperty.call(mark, 'from'); +} + @autoBindMethodsForReact(AUTOBIND_CFG) class CodeEditor extends Component { private _uniquenessKey?: string; @@ -238,7 +246,7 @@ class CodeEditor extends Component { } } - setCursor(ch, line = 0) { + setCursor(ch: number, line = 0) { if (this.codeMirror) { if (!this.hasFocus()) { this.focus(); @@ -296,7 +304,7 @@ class CodeEditor extends Component { getSelectionStart() { if (!this.codeMirror) { - return; + return null; } const selections = this.codeMirror.listSelections(); @@ -310,7 +318,7 @@ class CodeEditor extends Component { getSelectionEnd() { if (!this.codeMirror) { - return; + return null; } const selections = this.codeMirror.listSelections(); @@ -348,26 +356,23 @@ class CodeEditor extends Component { return; } - // @ts-expect-error this is a (Node & ParentNode) in the types, but I think it's actually supposed to be Element - this.codeMirror.getTextArea().parentNode?.setAttribute(name, value); + this.codeMirror.getTextArea().parentElement?.setAttribute(name, value); } - removeAttribute(name) { + removeAttribute(name: string) { if (!this.codeMirror) { return; } - // @ts-expect-error this is a (Node & ParentNode) in the types, but I think it's actually supposed to be Element - this.codeMirror.getTextArea().parentNode?.removeAttribute(name); + this.codeMirror.getTextArea().parentElement?.removeAttribute(name); } - getAttribute(name) { + getAttribute(name: string) { if (!this.codeMirror) { return; } - // @ts-expect-error this is a (Node & ParentNode) in the types, but I think it's actually supposed to be Element - this.codeMirror.getTextArea().parentNode?.getAttribute(name); + this.codeMirror.getTextArea().parentElement?.getAttribute(name); } clearSelection() { @@ -412,16 +417,20 @@ class CodeEditor extends Component { const marks = this.codeMirror .getAllMarks() - // @ts-expect-error -- TSCONVERSION - .filter(c => c.__isFold) + .filter(mark => mark.__isFold) .map(mark => { - // @ts-expect-error -- TSCONVERSION - const { from, to } = mark.find(); + const result = mark.find(); + + if (isMarkerRange(result)) { + return result; + } + return { - from, - to, + from: undefined, + to: undefined, }; }); + editorStates[uniquenessKey] = { scroll: this.codeMirror.getScrollInfo(), selections: this.codeMirror.listSelections(), @@ -472,7 +481,7 @@ class CodeEditor extends Component { return; } - const foldOptions = { + const foldOptions: CodeMirror.EditorConfiguration['foldOptions'] = { widget: (from, to) => { let count; // Get open / close token @@ -517,9 +526,7 @@ class CodeEditor extends Component { this.codeMirror.on('blur', this._codemirrorBlur); this.codeMirror.on('paste', this._codemirrorPaste); this.codeMirror.on('scroll', this._codemirrorScroll); - // @ts-expect-error this event does indeed exist, but is not present on the CodeMirror types and declaration merging doesn't seem to want to allow adding it this.codeMirror.on('fold', this._codemirrorToggleFold); - // @ts-expect-error this event does indeed exist, but is not present on the CodeMirror types and declaration merging doesn't seem to want to allow adding it this.codeMirror.on('unfold', this._codemirrorToggleFold); this.codeMirror.on('keyHandled', this._codemirrorKeyHandled); // Prevent these things if we're type === "password" @@ -530,16 +537,19 @@ class CodeEditor extends Component { line: -1, ch: -1, }); + + let extraKeys = BASE_CODEMIRROR_OPTIONS.extraKeys; + extraKeys = extraKeys && typeof extraKeys !== 'string' ? extraKeys : {}; + this.codeMirror.setOption('extraKeys', { - // @ts-expect-error -- TSCONVERSION - ...BASE_CODEMIRROR_OPTIONS.extraKeys, + ...extraKeys, Tab: cm => { // Indent with tabs or spaces // From https://github.com/codemirror/CodeMirror/issues/988#issuecomment-14921785 if (cm.somethingSelected()) { cm.indentSelection('add'); } else { - cm.replaceSelection(this._indentChars(), 'end', '+input'); + cm.replaceSelection(this._indentChars(), 'end'); } }, }); @@ -593,7 +603,7 @@ class CodeEditor extends Component { this.codeMirror.on('cursorActivity', this._codemirrorCursorActivity); } - static _isJSON(mode) { + static _isJSON(mode?: string) { if (!mode) { return false; } @@ -601,7 +611,7 @@ class CodeEditor extends Component { return mode.indexOf('json') !== -1; } - static _isYAML(mode) { + static _isYAML(mode?: string) { if (!mode) { return false; } @@ -609,7 +619,7 @@ class CodeEditor extends Component { return mode.indexOf('yaml') !== -1; } - static _isXML(mode) { + static _isXML(mode?: string) { if (!mode) { return false; } @@ -617,7 +627,7 @@ class CodeEditor extends Component { return mode.indexOf('xml') !== -1; } - static _isEDN(mode) { + static _isEDN(mode?: string) { if (!mode) { return false; } @@ -635,11 +645,11 @@ class CodeEditor extends Component { this._prettify(this.codeMirror?.getValue()); } - _prettify(code) { + _prettify(code?: string) { this._codemirrorSetValue(code, true); } - _prettifyJSON(code) { + _prettifyJSON(code: string) { try { let jsonString = code; @@ -661,7 +671,7 @@ class CodeEditor extends Component { } } - static _prettifyEDN(code) { + static _prettifyEDN(code: string) { try { return zprint(code, null); } catch (e) { @@ -669,7 +679,7 @@ class CodeEditor extends Component { } } - _prettifyXML(code) { + _prettifyXML(code: string) { if (this.props.updateFilter && this.state.filter) { try { const results = queryXPath(code, this.state.filter); @@ -733,11 +743,11 @@ class CodeEditor extends Component { // NOTE: YAML is not valid when indented with Tabs const isYaml = typeof rawMode === 'string' ? rawMode.includes('yaml') : false; const actuallyIndentWithTabs = indentWithTabs && !isYaml; - const options: any = { + const options: CodeMirror.EditorConfiguration = { readOnly: !!readOnly, placeholder: placeholder || '', mode: mode, - tabIndex: typeof tabIndex === 'number' ? tabIndex : null, + tabindex: typeof tabIndex === 'number' ? tabIndex : undefined, dragDrop: !noDragDrop, scrollbarStyle: hideScrollbars ? 'null' : 'native', styleActiveLine: !noStyleActiveLine, @@ -761,16 +771,18 @@ class CodeEditor extends Component { options.indentUnit = indentSize; } - if (!hideGutters && options.lint) { - options.gutters.push('CodeMirror-lint-markers'); - } + if (options.gutters && !hideGutters) { + if (options.lint) { + options.gutters.push('CodeMirror-lint-markers'); + } - if (!hideGutters && options.lineNumbers) { - options.gutters.push('CodeMirror-linenumbers'); + if (options.lineNumbers) { + options.gutters.push('CodeMirror-linenumbers'); + } } if (!hideGutters && options.foldGutter) { - options.gutters.push('CodeMirror-foldgutter'); + options.gutters?.push('CodeMirror-foldgutter'); } if (hintOptions) { @@ -795,7 +807,7 @@ class CodeEditor extends Component { // Setup the hint options if (getRenderContext || getAutocompleteConstants || getAutocompleteSnippets) { - let getVariables: (() => Promise) | undefined; + let getVariables: (() => Promise) | undefined; let getTags: (() => Promise) | undefined; if (getRenderContext) { @@ -818,8 +830,7 @@ class CodeEditor extends Component { } for (const option of firstArg.options || []) { - // @ts-expect-error -- TSCONVERSION option.name doesn't exist - const optionName = misc.fnOrString(option.displayName, tagDef.args) || option.name; + const optionName = misc.fnOrString(option.displayName, tagDef.args); const newDef = clone(tagDef); newDef.displayName = `${tagDef.displayName} ⇒ ${optionName}`; newDef.args[0].defaultValue = option.value; @@ -844,25 +855,26 @@ class CodeEditor extends Component { } // Strip of charset if there is one - Object.keys(options).map(key => { - this._codemirrorSmartSetOption(key, options[key]); - }); + Object.keys(options).map(key => + this._codemirrorSmartSetOption( + key as keyof CodeMirror.EditorConfiguration, + options[key] + ) + ); } /** * Set option if it's different than in the current Codemirror instance */ - _codemirrorSmartSetOption(key, value) { + _codemirrorSmartSetOption(key: K, value: CodeMirror.EditorConfiguration[K]) { const cm = this.codeMirror; let shouldSetOption = false; if (key === 'jump' || key === 'info' || key === 'lint' || key === 'hintOptions') { // Use stringify here because these could be infinitely recursive due to GraphQL // schemas - // @ts-expect-error -- TSCONVERSION - shouldSetOption = JSON.stringify(value) !== JSON.stringify(cm.options[key]); - // @ts-expect-error -- TSCONVERSION - } else if (!deepEqual(value, cm.options[key])) { + shouldSetOption = JSON.stringify(value) !== JSON.stringify(cm?.getOption(key)); + } else if (!deepEqual(value, cm?.getOption(key))) { // Don't set the option if it hasn't changed shouldSetOption = true; } @@ -875,8 +887,7 @@ class CodeEditor extends Component { // Set the option safely. When setting "lint", for example, it can throw an exception // and cause the editor to break. try { - // @ts-expect-error -- TSCONVERSION - cm.setOption(key, value); + cm?.setOption(key, value); } catch (err) { console.log('Failed to set CodeMirror option', err.message, { key, @@ -885,7 +896,7 @@ class CodeEditor extends Component { } } - static _normalizeMode(mode) { + static _normalizeMode(mode?: string) { const mimeType = mode ? mode.split(';')[0] : 'text/plain'; if (mimeType.includes('graphql-variables')) { @@ -910,15 +921,15 @@ class CodeEditor extends Component { } } - _codemirrorCursorActivity(instance) { + _codemirrorCursorActivity(instance: CodeMirror.EditorFromTextArea) { if (this.props.onCursorActivity) { this.props.onCursorActivity(instance); } } - async _codemirrorKeyDown(doc: CodeMirror.EditorFromTextArea, e) { + async _codemirrorKeyDown(doc: CodeMirror.EditorFromTextArea, e: KeyboardEvent & {codemirrorIgnore: boolean}) { // Use default tab behaviour if we're told - if (this.props.defaultTabBehavior && e.keyCode === TAB_KEY) { + if (this.props.defaultTabBehavior && e.code === TAB_KEY.toString()) { e.codemirrorIgnore = true; } @@ -933,9 +944,9 @@ class CodeEditor extends Component { } } - _codemirrorTriggerCompletionKeyUp(doc, e) { + _codemirrorTriggerCompletionKeyUp(doc: CodeMirror.EditorFromTextArea, e: KeyboardEvent) { // Enable graphql completion if we're in that mode - if (doc.options.mode === 'graphql') { + if (doc.getOption('mode') === 'graphql') { // Only operate on one-letter keys. This will filter out // any special keys (Backspace, Enter, etc) if (e.metaKey || e.ctrlKey || e.altKey || e.key.length > 1) { @@ -958,13 +969,13 @@ class CodeEditor extends Component { } } - _codemirrorFocus(_doc, e) { + _codemirrorFocus(_doc: CodeMirror.EditorFromTextArea, e: FocusEvent) { if (this.props.onFocus) { this.props.onFocus(e); } } - _codemirrorBlur(_doc, e) { + _codemirrorBlur(_doc: CodeMirror.EditorFromTextArea, e: FocusEvent) { this._persistState(); if (this.props.onBlur) { @@ -980,7 +991,7 @@ class CodeEditor extends Component { this._persistState(); } - _codemirrorKeyHandled(_codeMirror, _keyName, event) { + _codemirrorKeyHandled(_codeMirror: CodeMirror.EditorFromTextArea, _keyName: string, event: KeyboardEvent) { const { keyMap } = this.props; const { keyCode } = event; const isVimKeyMap = keyMap === EDITOR_KEY_MAP_VIM; @@ -991,7 +1002,7 @@ class CodeEditor extends Component { } } - _codemirrorValueBeforeChange(doc: CodeMirror.Editor, change: CodeMirror.EditorChangeCancellable) { + _codemirrorValueBeforeChange(doc: CodeMirror.EditorFromTextArea, change: CodeMirror.EditorChangeCancellable) { const value = this.codeMirror?.getDoc().getValue(); // If we're in single-line mode, merge all changed lines into one @@ -1004,7 +1015,7 @@ class CodeEditor extends Component { } // Don't allow non-breaking spaces because they break the GraphQL syntax - if (doc.getOption('mode') === 'graphql') { + if (doc.getOption('mode') === 'graphql' && change.text.length > 0) { const text = change.text.map(normalizeIrregularWhitespace); change.update?.(change.from, change.to, text); @@ -1018,13 +1029,13 @@ class CodeEditor extends Component { } } - _codemirrorPaste(_cm, e) { + _codemirrorPaste(_cm: CodeMirror.EditorFromTextArea, e: ClipboardEvent) { if (this.props.onPaste) { this.props.onPaste(e); } } - _codemirrorPreventWhenTypePassword(_cm, e) { + _codemirrorPreventWhenTypePassword(_cm: CodeMirror.EditorFromTextArea, e: Event) { const { type } = this.props; if (type && type.toLowerCase() === 'password') { @@ -1046,8 +1057,8 @@ class CodeEditor extends Component { // Disable linting if the document reaches a maximum size or is empty const isOverMaxSize = value.length > MAX_SIZE_FOR_LINTING; const shouldLint = isOverMaxSize || value.length === 0 ? false : !this.props.noLint; - // @ts-expect-error TSCONVERSION - const existingLint = this.codeMirror?.options.lint || false; + + const existingLint = this.codeMirror?.getOption('lint') || false; if (shouldLint !== existingLint) { const { lintOptions } = this.props; @@ -1063,7 +1074,7 @@ class CodeEditor extends Component { * @param code the code to set in the editor * @param forcePrettify */ - _codemirrorSetValue(code, forcePrettify = false) { + _codemirrorSetValue(code = '', forcePrettify = false) { if (typeof code !== 'string') { console.warn('Code editor was passed non-string value', code); return; @@ -1089,10 +1100,10 @@ class CodeEditor extends Component { } } - this.codeMirror?.setValue(code || ''); + this.codeMirror?.setValue(code); } - _handleFilterHistorySelect(filter) { + _handleFilterHistorySelect(filter = '') { this._filterInput.value = filter; this._setFilter(filter); @@ -1102,7 +1113,7 @@ class CodeEditor extends Component { this._setFilter(e.target.value); } - _setFilter(filter) { + _setFilter(filter = '') { if (this._filterTimeout !== null) { clearTimeout(this._filterTimeout); } diff --git a/packages/insomnia-app/app/ui/components/codemirror/extensions/autocomplete.ts b/packages/insomnia-app/app/ui/components/codemirror/extensions/autocomplete.ts index 9a550bbfab..54003b3340 100644 --- a/packages/insomnia-app/app/ui/components/codemirror/extensions/autocomplete.ts +++ b/packages/insomnia-app/app/ui/components/codemirror/extensions/autocomplete.ts @@ -1,10 +1,11 @@ import 'codemirror/addon/mode/overlay'; -import CodeMirror from 'codemirror'; +import CodeMirror, { EnvironmentAutocompleteOptions, Hint, ShowHintOptions } from 'codemirror'; import { escapeHTML, escapeRegex, isNotNullOrUndefined } from '../../../../common/misc'; import * as models from '../../../../models'; -import { getDefaultFill } from '../../../../templating/utils'; +import { getDefaultFill, NunjucksParsedTag } from '../../../../templating/utils'; +import { isNunjucksMode } from '../modes/nunjucks'; const NAME_MATCH_FLEXIBLE = /[\w.\][\-/]+$/; const NAME_MATCH = /[\w.\][]+$/; @@ -54,14 +55,14 @@ CodeMirror.defineExtension('closeHintDropdown', function() { this.state.completionActive?.close(); }); -CodeMirror.defineOption('environmentAutocomplete', null, (cm, options) => { +CodeMirror.defineOption('environmentAutocomplete', null, (cm: CodeMirror.EditorFromTextArea, options: EnvironmentAutocompleteOptions) => { if (!options) { return; } - async function completeAfter(cm: CodeMirror.Editor, fn, showAllOnNoMatch = false) { + async function completeAfter(cm: CodeMirror.EditorFromTextArea, callback?: () => boolean, showAllOnNoMatch = false) { // Bail early if didn't match the callback test - if (fn && !fn()) { + if (callback && !callback()) { return; } @@ -91,7 +92,6 @@ CodeMirror.defineOption('environmentAutocomplete', null, (cm, options) => { // Actually show the hint cm.showHint({ // Insomnia-specific options - // @ts-expect-error -- TSCONVERSION needs investigation constants: constants || [], variables: variables || [], snippets: snippets || [], @@ -114,37 +114,39 @@ CodeMirror.defineOption('environmentAutocomplete', null, (cm, options) => { }); } - function completeIfInVariableName(cm) { + function completeIfInVariableName(cm: CodeMirror.EditorFromTextArea) { completeAfter(cm, () => { const cur = cm.getCursor(); const pos = CodeMirror.Pos(cur.line, cur.ch - MAX_HINT_LOOK_BACK); const range = cm.getRange(pos, cur); - return range.match(COMPLETE_AFTER_WORD); + + return COMPLETE_AFTER_WORD.test(range); }); return CodeMirror.Pass; } - function completeIfAfterTagOrVarOpen(cm) { + function completeIfAfterTagOrVarOpen(cm: CodeMirror.EditorFromTextArea) { completeAfter( cm, () => { const cur = cm.getCursor(); const pos = CodeMirror.Pos(cur.line, cur.ch - MAX_HINT_LOOK_BACK); const range = cm.getRange(pos, cur); - return range.match(COMPLETE_AFTER_CURLIES); + + return COMPLETE_AFTER_CURLIES.test(range); }, true, ); return CodeMirror.Pass; } - function completeForce(cm) { - completeAfter(cm, null, true); + function completeForce(cm: CodeMirror.EditorFromTextArea) { + completeAfter(cm, undefined, true); return CodeMirror.Pass; } - let keydownDebounce: NodeJS.Timeout | null = null; - cm.on('keydown', async (cm, e) => { + let keydownTimeoutHandle: NodeJS.Timeout | null = null; + cm.on('keydown', async (cm: CodeMirror.EditorFromTextArea, e) => { // Close autocomplete on Escape if it's open if (cm.isHintDropdownActive() && e.key === 'Escape') { if (!cm.state.completionActive) { @@ -162,21 +164,21 @@ CodeMirror.defineOption('environmentAutocomplete', null, (cm, options) => { return; } - if (keydownDebounce !== null) { - clearTimeout(keydownDebounce); + if (keydownTimeoutHandle !== null) { + clearTimeout(keydownTimeoutHandle); } const { autocompleteDelay } = await models.settings.getOrCreate(); if (autocompleteDelay > 0) { - keydownDebounce = setTimeout(() => { + keydownTimeoutHandle = setTimeout(() => { completeIfInVariableName(cm); }, autocompleteDelay); } }); // Clear timeout if we already closed the completion cm.on('endCompletion', () => { - if (keydownDebounce !== null) { - clearTimeout(keydownDebounce); + if (keydownTimeoutHandle !== null) { + clearTimeout(keydownTimeoutHandle); } }); // Remove keymap if we're already added it @@ -196,12 +198,12 @@ CodeMirror.defineOption('environmentAutocomplete', null, (cm, options) => { * @param options * @returns {Promise.<{list: Array, from, to}>} */ -function hint(cm, options) { +function hint(cm: CodeMirror.EditorFromTextArea, options: ShowHintOptions) { // Add type to all things (except constants, which need to convert to an object) - const variablesToMatch = (options.variables || []).map(v => ({ ...v, type: TYPE_VARIABLE })); - const snippetsToMatch = (options.snippets || []).map(v => ({ ...v, type: TYPE_SNIPPET })); - const tagsToMatch = (options.tags || []).map(v => ({ ...v, type: TYPE_TAG })); - const constantsToMatch = (options.constants || []).map(s => ({ + const variablesToMatch: VariableCompletionItem[] = (options.variables || []).map(v => ({ ...v, type: TYPE_VARIABLE })); + const snippetsToMatch: SnippetCompletionItem[] = (options.snippets || []).map(v => ({ ...v, type: TYPE_SNIPPET })); + const tagsToMatch: TagCompletionItem[] = (options.tags || []).map(v => ({ ...v, type: TYPE_TAG })); + const constantsToMatch: ConstantCompletionItem[] = (options.constants || []).map(s => ({ name: s, value: s, displayValue: '', @@ -228,24 +230,27 @@ function hint(cm, options) { const nameSegmentLong = nameMatchLong ? nameMatchLong[0] : fallbackSegment; const nameSegmentFull = previousText; // Actually try to match the list of things - const lowPriorityMatches = []; - const highPriorityMatches = []; - let mode; + let lowPriorityMatches: Hint[] = []; + let highPriorityMatches: Hint[] = []; - try { - mode = cm.getOption('mode').baseMode; - } catch (e) { - mode = 'unknown'; + const modeOption = cm.getOption('mode') ?? 'unknown'; + + let mode = 'unknown'; + + if (typeof modeOption === 'string') { + mode = modeOption; + } else if (isNunjucksMode(modeOption)) { + mode = modeOption.baseMode; } // Match variables if (allowMatchingVariables) { - matchSegments(variablesToMatch, nameSegment, TYPE_VARIABLE, MAX_VARIABLES).forEach(m => - lowPriorityMatches.push(m), - ); - matchSegments(variablesToMatch, nameSegmentLong, TYPE_VARIABLE, MAX_VARIABLES).forEach(m => - highPriorityMatches.push(m), - ); + const sortVariableCompletionHints = getCompletionHints(variablesToMatch, nameSegment, TYPE_VARIABLE, MAX_VARIABLES); + lowPriorityMatches = [...lowPriorityMatches, ...sortVariableCompletionHints]; + + const longVariableCompletionHints = getCompletionHints(variablesToMatch, nameSegmentLong, TYPE_VARIABLE, MAX_VARIABLES); + + highPriorityMatches = [...highPriorityMatches, ...longVariableCompletionHints]; } // Match constants @@ -262,48 +267,48 @@ function hint(cm, options) { // Remove trailing quotes and spaces if (token.type === 'variable') { // We're inside a JSON key - matchSegments(constantsToMatch, segment, TYPE_CONSTANT, MAX_CONSTANTS).forEach(m => - highPriorityMatches.push(m), - ); + const constantCompletionHints = getCompletionHints(constantsToMatch, segment, TYPE_CONSTANT, MAX_CONSTANTS); + highPriorityMatches = [...highPriorityMatches, ...constantCompletionHints]; + } else if ( token.type === 'invalidchar' || token.type === 'ws' || (token.type === 'punctuation' && token.string === '{') ) { // We're outside of a JSON key - matchSegments(constantsToMatch, segment, TYPE_CONSTANT, MAX_CONSTANTS).forEach(m => - // @ts-expect-error -- TSCONVERSION - highPriorityMatches.push({ ...m, text: '"' + m.text + '": ' }), - ); + const constantCompletionHints = getCompletionHints(constantsToMatch, segment, TYPE_CONSTANT, MAX_CONSTANTS).map(hint => ({ ...hint, text: '"' + hint.text + '": ' })); + + highPriorityMatches = [...highPriorityMatches, ...constantCompletionHints]; } } else { // Otherwise match full segments - matchSegments(constantsToMatch, nameSegmentFull, TYPE_CONSTANT, MAX_CONSTANTS).forEach(m => - highPriorityMatches.push(m), - ); + const hints = getCompletionHints(constantsToMatch, nameSegmentFull, TYPE_CONSTANT, MAX_CONSTANTS); + + highPriorityMatches = [...highPriorityMatches, ...hints]; } } // Match tags if (allowMatchingTags) { - matchSegments(tagsToMatch, nameSegment, TYPE_TAG, MAX_TAGS).forEach(m => - lowPriorityMatches.push(m), - ); - matchSegments(tagsToMatch, nameSegmentLong, TYPE_TAG, MAX_TAGS).forEach(m => - highPriorityMatches.push(m), - ); + const lowPriorityTagHints = getCompletionHints(tagsToMatch, nameSegment, TYPE_TAG, MAX_TAGS); + + lowPriorityMatches = [...lowPriorityMatches, ...lowPriorityTagHints]; + + const highPriorityTagHints = getCompletionHints(tagsToMatch, nameSegmentLong, TYPE_TAG, MAX_TAGS); + + highPriorityMatches = [...highPriorityMatches, ...highPriorityTagHints]; } - matchSegments(snippetsToMatch, nameSegment, TYPE_SNIPPET, MAX_SNIPPETS).forEach(m => - highPriorityMatches.push(m), - ); + const snippetHints = getCompletionHints(snippetsToMatch, nameSegment, TYPE_SNIPPET, MAX_SNIPPETS); + + highPriorityMatches = [...highPriorityMatches, ...snippetHints]; + const matches = [...highPriorityMatches, ...lowPriorityMatches]; // Autocomplete from longest matched segment const segment = highPriorityMatches.length ? nameSegmentLong : nameSegment; const uniqueMatches = matches.reduce( - // @ts-expect-error -- TSCONVERSION (arr, v) => (arr.find(a => a.text === v.text) ? arr : [...arr, v]), - [], // Default value + [] as Hint[], // Default value ); return { list: uniqueMatches, @@ -313,13 +318,13 @@ function hint(cm, options) { } /** - * Replace the text in the editor when a hint is selected. + * Replace the text in the EditorFromTextArea when a hint is selected. * This also makes sure there is whitespace surrounding it * @param cm * @param self * @param data */ -async function replaceHintMatch(cm, _self, data) { +async function replaceHintMatch(cm: CodeMirror.EditorFromTextArea, _self, data) { if (typeof data.text === 'function') { data.text = await data.text(); } @@ -361,79 +366,92 @@ async function replaceHintMatch(cm, _self, data) { cm.replaceRange(`${prefix}${data.text}${suffix}`, from, to); } -/** - * Match against a list of things - * @param listOfThings - Can be list of strings or list of {name, value} - * @param segment - segment to match against - * @param type - * @param limit - * @returns {Array} - */ -function matchSegments(listOfThings, segment, type, limit = -1) { - if (!Array.isArray(listOfThings)) { - console.warn('Autocomplete received items in non-list form', listOfThings); - return []; - } +// @TODO Simplify these types and make the interface stricter and move the transforms in the top level. +interface CompletionItemKind { + name: string; + displayName?: string; + displayValue?: string; + value?: string | (() => PromiseLike); + type: typeof TYPE_CONSTANT | typeof TYPE_SNIPPET | typeof TYPE_VARIABLE | typeof TYPE_TAG; +} - const matches = []; +interface ConstantCompletionItem extends CompletionItemKind { + type: typeof TYPE_CONSTANT; + value: string; +} - for (const t of listOfThings) { - const name = typeof t === 'string' ? t : t.name; - const value = typeof t === 'string' ? '' : t.value; - const displayName = t.displayName || name; - // Generate the value we'll fill it with - let defaultFill; +interface VariableCompletionItem extends CompletionItemKind { + type: typeof TYPE_VARIABLE; + value: string | (() => PromiseLike); +} - if (t.type === TYPE_CONSTANT) { - defaultFill = t.value; - } else if (t.type === TYPE_SNIPPET) { - defaultFill = t.value; - } else if (t.type === TYPE_VARIABLE) { - // Variables fill with variable name, not value (eg. {{ foo }}) - // TODO: This is extremely confusing and does not make any sense so let's - // refactor this to a single unified format for all types - defaultFill = t.name; - } else if (t.type === TYPE_TAG) { - defaultFill = getDefaultFill(t.name, t.args); - } else { - throw new Error('Unidentified autocomplete type: ' + t.type); +interface SnippetCompletionItem extends CompletionItemKind { + type: typeof TYPE_SNIPPET; + value: string | (() => PromiseLike); +} + +interface TagCompletionItem extends NunjucksParsedTag, CompletionItemKind { + type: typeof TYPE_TAG; +} + +type CompletionItem = ConstantCompletionItem | VariableCompletionItem | SnippetCompletionItem | TagCompletionItem; + +function isConstantCompletionItem(item: CompletionItem): item is ConstantCompletionItem { + return item.type === TYPE_CONSTANT; +} + +function isVariableCompletionItem(item: CompletionItem): item is VariableCompletionItem { + return item.type === TYPE_VARIABLE; +} + +function isSnippetCompletionItem(item: CompletionItem): item is SnippetCompletionItem { + return item.type === TYPE_SNIPPET; +} + +function isTagCompletionItem(item: CompletionItem): item is TagCompletionItem { + return item.type === TYPE_TAG; +} + +function getCompletionHints(completionItems: CompletionItem[], segment: string, type: CompletionItem['type'], limit = -1): Hint[] { + const matches: CodeMirror.Hint[] = []; + + for (const item of completionItems) { + const name = typeof item === 'string' ? item : item.name; + const value = typeof item === 'string' ? '' : item.value ?? ''; + const displayName = item.displayName || name; + let defaultFill = ''; + + if (isConstantCompletionItem(item)) { + defaultFill = item.value; + } else if (isVariableCompletionItem(item) || isSnippetCompletionItem(item)) { + defaultFill = item.name; + } else if (isTagCompletionItem(item)) { + defaultFill = getDefaultFill(item.name, item.args); } const matchSegment = segment.toLowerCase(); - const matchName = displayName.toLowerCase(); + const matchName = displayName.toLowerCase() || ''; // Throw away things that don't match if (!matchName.includes(matchSegment)) { continue; } - let displayValue = t.displayValue; + let displayValue = item.displayValue || ''; - if (typeof displayValue !== 'string' && typeof value !== 'function') { + if (typeof item.displayValue !== 'string' && typeof value !== 'function') { displayValue = JSON.stringify(value); } matches.push({ - // Custom Insomnia keys - // @ts-expect-error -- TSCONVERSION type, - // @ts-expect-error -- TSCONVERSION segment, - // @ts-expect-error -- TSCONVERSION - displayValue, - // @ts-expect-error -- TSCONVERSION - comment: value, - // @ts-expect-error -- TSCONVERSION + displayValue: displayValue, + comment: value.toString(), score: name.length, - // In case we want to sort by this - // CodeMirror - // @ts-expect-error -- TSCONVERSION text: defaultFill, - // @ts-expect-error -- TSCONVERSION - displayText: displayName, - // @ts-expect-error -- TSCONVERSION + displayText: displayName || name, render: renderHintMatch, - // @ts-expect-error -- TSCONVERSION hint: replaceHintMatch, }); } @@ -447,13 +465,8 @@ function matchSegments(listOfThings, segment, type, limit = -1) { /** * Replace all occurrences of string - * @param text - * @param find - * @param prefix - * @param suffix - * @returns string */ -function replaceWithSurround(text, find, prefix, suffix) { +function replaceWithSurround(text: string, find: string, prefix: string, suffix: string) { const escapedString = escapeRegex(find); const re = new RegExp(escapedString, 'gi'); return text.replace(re, matched => prefix + matched + suffix); @@ -461,17 +474,14 @@ function replaceWithSurround(text, find, prefix, suffix) { /** * Render the autocomplete list entry - * @param li - * @param self - * @param data */ -function renderHintMatch(li, _self, data) { +function renderHintMatch(li: HTMLElement, _allHints: CodeMirror.Hints, hint: Hint) { // Bold the matched text - const { displayText, segment, displayValue } = data; - const markedName = replaceWithSurround(displayText, segment, '', ''); - const { char, title } = ICONS[data.type]; - + const { displayText, segment, type, displayValue } = hint; + const markedName = replaceWithSurround(displayText || '', segment, '', ''); + const { char, title } = ICONS[type]; let safeValue = ''; + if (isNotNullOrUndefined(displayValue)) { const escaped = escapeHTML(displayValue); safeValue = ` @@ -481,7 +491,7 @@ function renderHintMatch(li, _self, data) { `; } - li.className += ` fancy-hint type--${data.type}`; + li.className += ` fancy-hint type--${type}`; li.innerHTML = `
${markedName}
diff --git a/packages/insomnia-app/app/ui/components/codemirror/modes/nunjucks.ts b/packages/insomnia-app/app/ui/components/codemirror/modes/nunjucks.ts index 1ab96235d7..160ec19f98 100644 --- a/packages/insomnia-app/app/ui/components/codemirror/modes/nunjucks.ts +++ b/packages/insomnia-app/app/ui/components/codemirror/modes/nunjucks.ts @@ -1,4 +1,11 @@ import CodeMirror from 'codemirror'; + +export function isNunjucksMode( + mode: CodeMirror.ModeSpec +): mode is CodeMirror.ModeSpec<{ baseMode: 'string' }> { + return 'baseMode' in mode; +} + CodeMirror.defineMode('nunjucks', (config, parserConfig) => { const baseMode = CodeMirror.getMode(config, parserConfig.baseMode || 'text/plain'); diff --git a/packages/insomnia-app/app/ui/components/codemirror/one-line-editor.tsx b/packages/insomnia-app/app/ui/components/codemirror/one-line-editor.tsx index 3fa00acb02..8a4a573f01 100644 --- a/packages/insomnia-app/app/ui/components/codemirror/one-line-editor.tsx +++ b/packages/insomnia-app/app/ui/components/codemirror/one-line-editor.tsx @@ -4,8 +4,9 @@ import React, { Fragment, PureComponent } from 'react'; import ReactDOM from 'react-dom'; import { AUTOBIND_CFG } from '../../../common/constants'; +import { HandleGetRenderContext, HandleRender } from '../../../common/render'; import Input from '../base/debounced-input'; -import CodeEditor from './code-editor'; +import CodeEditor, { CodeEditorOnChange } from './code-editor'; const MODE_INPUT = 'input'; const MODE_EDITOR = 'editor'; const TYPE_TEXT = 'text'; @@ -16,20 +17,21 @@ interface Props { id?: string; type?: string; mode?: string; - onBlur?: Function; - onKeyDown?: Function; - onFocus?: Function; - onChange?: Function; - onPaste?: Function; - render?: Function; - getRenderContext?: Function; + onBlur?: (e: FocusEvent) => void; + onKeyDown?: (e: KeyboardEvent, value?: any) => void; + onFocus?: (e: FocusEvent) => void; + onChange?: CodeEditorOnChange; + onPaste?: (e: ClipboardEvent) => void; + render?: HandleRender; + getRenderContext?: HandleGetRenderContext; nunjucksPowerUserMode?: boolean; - getAutocompleteConstants?: Function | null; + getAutocompleteConstants?: () => string[] | PromiseLike; placeholder?: string; className?: string; forceEditor?: boolean; forceInput?: boolean; isVariableUncovered?: boolean; + readOnly?: boolean; // TODO(TSCONVERSION) figure out why so many components pass this in yet it isn't used anywhere in this disabled?: boolean; } @@ -218,14 +220,14 @@ class OneLineEditor extends PureComponent { } } - _handleInputBlur() { + _handleInputBlur(e: FocusEvent) { // Set focused state this._input?.removeAttribute('data-focused'); - this.props.onBlur?.(); + this.props.onBlur?.(e); } - _handleEditorBlur() { + _handleEditorBlur(e: FocusEvent) { // Editor was already removed from the DOM, so do nothing if (!this._editor) { return; @@ -244,9 +246,10 @@ class OneLineEditor extends PureComponent { }, 2000); } - this.props.onBlur?.(); + this.props.onBlur?.(e); } + // @TODO Refactor this event handler. The way we search for a parent form node is not stable. _handleKeyDown(e) { // submit form if needed if (e.keyCode === 13) { @@ -382,14 +385,10 @@ class OneLineEditor extends PureComponent { onKeyDown={this._handleKeyDown} onFocus={this._handleEditorFocus} onMouseLeave={this._handleEditorMouseLeave} - // @ts-expect-error -- TSCONVERSION onChange={onChange} - // @ts-expect-error -- TSCONVERSION render={render} - // @ts-expect-error -- TSCONVERSION getRenderContext={getRenderContext} nunjucksPowerUserMode={nunjucksPowerUserMode} - // @ts-expect-error -- TSCONVERSION getAutocompleteConstants={getAutocompleteConstants} className={classnames('editor--single-line', className)} defaultValue={defaultValue} diff --git a/packages/insomnia-app/app/ui/components/editors/auth/o-auth-1-auth.tsx b/packages/insomnia-app/app/ui/components/editors/auth/o-auth-1-auth.tsx index c841f3b74a..2521428b34 100644 --- a/packages/insomnia-app/app/ui/components/editors/auth/o-auth-1-auth.tsx +++ b/packages/insomnia-app/app/ui/components/editors/auth/o-auth-1-auth.tsx @@ -193,7 +193,7 @@ class OAuth1Auth extends PureComponent { property: string, onChange: (...args: any[]) => any, help: string | null = null, - handleAutocomplete: ((...args: any[]) => any) | null = null, + handleAutocomplete?: ((...args: any[]) => any) ) { const { handleRender, diff --git a/packages/insomnia-app/app/ui/components/editors/auth/o-auth-2-auth.tsx b/packages/insomnia-app/app/ui/components/editors/auth/o-auth-2-auth.tsx index ff0b3777a8..a4b9eff177 100644 --- a/packages/insomnia-app/app/ui/components/editors/auth/o-auth-2-auth.tsx +++ b/packages/insomnia-app/app/ui/components/editors/auth/o-auth-2-auth.tsx @@ -320,7 +320,7 @@ class OAuth2Auth extends PureComponent { property: string, onChange: (...args: any[]) => any, help: string | null = null, - handleAutocomplete: ((...args: any[]) => any) | null = null, + handleAutocomplete?: (...args: any[]) => any, ) { const { handleRender, diff --git a/packages/insomnia-app/app/ui/components/editors/body/graph-ql-editor.tsx b/packages/insomnia-app/app/ui/components/editors/body/graph-ql-editor.tsx index 74fc38dadc..3384f879e4 100644 --- a/packages/insomnia-app/app/ui/components/editors/body/graph-ql-editor.tsx +++ b/packages/insomnia-app/app/ui/components/editors/body/graph-ql-editor.tsx @@ -1,10 +1,13 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; import classnames from 'classnames'; -import { EditorFromTextArea, TextMarker } from 'codemirror'; +import { EditorFromTextArea, LintOptions, ShowHintOptions, TextMarker } from 'codemirror'; +import { GraphQLInfoOptions } from 'codemirror-graphql/info'; +import { ModifiedGraphQLJumpOptions } from 'codemirror-graphql/jump'; import electron, { OpenDialogOptions } from 'electron'; import { readFileSync } from 'fs'; -import { GraphQLSchema } from 'graphql'; +import { DefinitionNode, DocumentNode, GraphQLNonNull, GraphQLSchema, NonNullTypeNode, OperationDefinitionNode } from 'graphql'; import { parse, print, typeFromAST } from 'graphql'; +import Maybe from 'graphql/tsutils/Maybe'; import { buildClientSchema, getIntrospectionQuery } from 'graphql/utilities'; import { json as jsonPrettify } from 'insomnia-prettify'; import React, { PureComponent } from 'react'; @@ -13,7 +16,7 @@ import ReactDOM from 'react-dom'; import { AUTOBIND_CFG, CONTENT_TYPE_JSON, DEBOUNCE_MILLIS } from '../../../../common/constants'; import { database as db } from '../../../../common/database'; import { markdownToHTML } from '../../../../common/markdown-to-html'; -import { jsonParseOr } from '../../../../common/misc'; +import { isNotNullOrUndefined, jsonParseOr } from '../../../../common/misc'; import { HandleGetRenderContext, HandleRender } from '../../../../common/render'; import * as models from '../../../../models/index'; import type { Request } from '../../../../models/request'; @@ -37,6 +40,10 @@ if (!explorerContainer) { throw new Error('Failed to find #graphql-explorer-container'); } +function isOperationDefinition(def: DefinitionNode): def is OperationDefinitionNode { + return def.kind === 'OperationDefinition'; +} + interface GraphQLBody { query: string; variables?: Record; @@ -76,7 +83,7 @@ interface State { @autoBindMethodsForReact(AUTOBIND_CFG) class GraphQLEditor extends PureComponent { _disabledOperationMarkers: TextMarker[] = []; - _documentAST: null | Record = null; + _documentAST: null | DocumentNode = null; _isMounted = false; _queryEditor: null | EditorFromTextArea = null; _schemaFetchTimeout: NodeJS.Timeout | null = null; @@ -86,13 +93,17 @@ class GraphQLEditor extends PureComponent { const body = GraphQLEditor._stringToGraphQL(props.content); this._setDocumentAST(body.query); - let automaticFetch; + let automaticFetch = true; try { - // @ts-expect-error -- TSCONVERSION don't parse if the read item is not defined - automaticFetch = JSON.parse(window.localStorage.getItem('graphql.automaticFetch')); + const automaticFetchStringified = window.localStorage.getItem('graphql.automaticFetch'); + if (automaticFetchStringified) { + automaticFetch = JSON.parse(automaticFetchStringified); + } } catch (err) { - automaticFetch = true; + if (err instanceof Error) { + console.warn('Could not parse value of graphql.automaticFetch from localStorage:', err.message); + } } this.state = { @@ -163,12 +174,16 @@ class GraphQLEditor extends PureComponent { }); } - _handleClickReference(reference: ActiveReference, event: React.MouseEvent) { + _handleClickReference(reference: Maybe, event: MouseEvent) { event.preventDefault(); - this.setState({ - explorerVisible: true, - activeReference: reference, - }); + + if (reference) { + this.setState({ + explorerVisible: true, + activeReference: reference, + }); + } + } _handleQueryFocus() { @@ -189,22 +204,25 @@ class GraphQLEditor extends PureComponent { const { _documentAST, _queryEditor } = this; if (!_documentAST || !_queryEditor) { - return null; + return; } - const disabledDefinitions = _documentAST.definitions.filter(d => { - const name = d.name ? d.name.value : null; - return d.kind === 'OperationDefinition' && name !== operationName; - }); - // Remove current query highlighting for (const textMarker of this._disabledOperationMarkers) { textMarker.clear(); } + const disabledDefinitionLocations = _documentAST.definitions + .filter(isOperationDefinition) + .filter(d => { + return d.name?.value !== operationName; + }) + .map(definition => definition.loc) + .filter(isNotNullOrUndefined); + // Add "Unhighlight" markers - this._disabledOperationMarkers = disabledDefinitions.map(definition => { - const { startToken, endToken } = definition.loc; + const disabledOperationMarkers = disabledDefinitionLocations.map(location => { + const { startToken, endToken } = location; const from = { line: startToken.line - 1, ch: startToken.column - 1, @@ -213,13 +231,13 @@ class GraphQLEditor extends PureComponent { line: endToken.line, ch: endToken.column - 1, }; - // @ts-expect-error - doc doesn't exist, use getDoc() - return _queryEditor.doc.markText(from, to, { + + return _queryEditor.getDoc().markText(from, to, { className: 'cm-gql-disabled', }); }); - return null; + this._disabledOperationMarkers = disabledOperationMarkers; } _handleViewResponse() { @@ -371,27 +389,26 @@ class GraphQLEditor extends PureComponent { } _buildVariableTypes( - schema: Record | null, - ): Record> | null { + schema: GraphQLSchema | null, + ): Record> | null { if (!schema) { return null; } const definitions = this._documentAST ? this._documentAST.definitions : []; - const variableToType = {}; + const variableToType: Record> = {}; - for (const { kind, variableDefinitions } of definitions) { - if (kind !== 'OperationDefinition') { + for (const definition of definitions) { + if (!isOperationDefinition(definition)) { continue; } - if (!variableDefinitions) { + if (!definition.variableDefinitions) { continue; } - for (const { variable, type } of variableDefinitions) { - // @ts-expect-error -- TSCONVERSION - const inputType = typeFromAST(schema, type); + for (const { variable, type } of definition.variableDefinitions) { + const inputType = typeFromAST(schema, type as NonNullTypeNode); if (!inputType) { continue; @@ -432,8 +449,8 @@ class GraphQLEditor extends PureComponent { this.setState({ automaticFetch, }); - // @ts-expect-error -- TSCONVERSION convert boolean to string when setting - window.localStorage.setItem('graphql.automaticFetch', automaticFetch); + + window.localStorage.setItem('graphql.automaticFetch', automaticFetch.toString()); } _handlePrettify() { @@ -651,17 +668,48 @@ class GraphQLEditor extends PureComponent { const variableTypes = this._buildVariableTypes(schema); // Create portal for GraphQL Explorer - const graphQLExplorerPortal = ReactDOM.createPortal( - , - // @ts-expect-error -- TSCONVERSION - explorerContainer, - ); + let graphQLExplorerPortal: React.ReactPortal | null = null; + if (explorerContainer) { + graphQLExplorerPortal = ReactDOM.createPortal( + , + explorerContainer + ); + } + + let graphqlOptions: { + hintOptions: ShowHintOptions; + infoOptions: GraphQLInfoOptions; + jumpOptions: ModifiedGraphQLJumpOptions; + lintOptions: LintOptions; + } | undefined; + + if (schema) { + graphqlOptions = { + hintOptions: { + schema, + completeSingle: false, + }, + infoOptions: { + schema, + renderDescription: GraphQLEditor.renderMarkdown, + onClick: this._handleClickReference, + }, + jumpOptions: { + schema, + onClick: this._handleClickReference, + }, + lintOptions: { + schema, + }, + }; + } + return (
@@ -701,26 +749,6 @@ class GraphQLEditor extends PureComponent { dynamicHeight manualPrettify uniquenessKey={uniquenessKey ? uniquenessKey + '::query' : undefined} - hintOptions={{ - schema: schema || null, - completeSingle: false, - }} - infoOptions={{ - schema: schema || null, - renderDescription: GraphQLEditor.renderMarkdown, - onClick: this._handleClickReference, - }} - jumpOptions={{ - schema: schema || null, - onClick: this._handleClickReference, - }} - lintOptions={ - schema - ? { - schema, - } - : undefined - } fontSize={settings.editorFontSize} indentSize={settings.editorIndentSize} keyMap={settings.editorKeyMap} @@ -733,6 +761,7 @@ class GraphQLEditor extends PureComponent { mode="graphql" lineWrapping={settings.editorLineWrapping} placeholder="" + {...graphqlOptions} />
diff --git a/packages/insomnia-app/app/ui/components/graph-ql-explorer/graph-ql-explorer.tsx b/packages/insomnia-app/app/ui/components/graph-ql-explorer/graph-ql-explorer.tsx index 2e33d75da3..f4c5791698 100644 --- a/packages/insomnia-app/app/ui/components/graph-ql-explorer/graph-ql-explorer.tsx +++ b/packages/insomnia-app/app/ui/components/graph-ql-explorer/graph-ql-explorer.tsx @@ -1,5 +1,6 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; -import { GraphQLSchema, GraphQLType } from 'graphql'; +import { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; +import { GraphQLField, GraphQLNamedType, GraphQLSchema, GraphQLType, isNamedType } from 'graphql'; import { GraphQLEnumType } from 'graphql'; import React, { PureComponent } from 'react'; import { createRef } from 'react'; @@ -16,6 +17,46 @@ import GraphQLExplorerSearchResults from './graph-ql-explorer-search-results'; import GraphQLExplorerType from './graph-ql-explorer-type'; import { ActiveReference, GraphQLFieldWithParentName } from './graph-ql-types'; +function getReferenceInfo(reference: SchemaReference) { + let field: GraphQLField | undefined; + if ('field' in reference) { + field = reference.field; + } + + let type: GraphQLType | undefined; + if ('type' in reference) { + type = reference.type; + } + + return { type, field }; +} + +function isSameFieldAndType( + currentType?: GraphQLType, + type?: GraphQLType, + currentField?: GraphQLFieldWithParentName, + field?: GraphQLField +) { + // @TODO Simplify this function since it's hard to follow along + const compare = < + T extends GraphQLNamedType | GraphQLFieldWithParentName, + U extends GraphQLNamedType | GraphQLFieldWithParentName + >(a?: T, b?: U) => (!a && !b) || (a && b && a.name === b.name); + + if (!isNamedType(currentType)) { + currentType = undefined; + } + + if (!isNamedType(type)) { + type = undefined; + } + + const isSameType = compare(currentType, type); + + const isSameField = compare(currentField, field); + return isSameType && isSameField; +} + interface Props { handleClose: () => void; schema: GraphQLSchema | null; @@ -24,8 +65,8 @@ interface Props { } interface HistoryItem { - currentType: null | GraphQLType; - currentField: null | GraphQLFieldWithParentName; + currentType?: GraphQLType; + currentField?: GraphQLFieldWithParentName; } interface State extends HistoryItem { @@ -39,8 +80,8 @@ const SEARCH_UPDATE_DELAY_IN_MS = 300; class GraphQLExplorer extends PureComponent { state: State = { history: [], - currentType: null, - currentField: null, + currentType: undefined, + currentField: undefined, filter: '', }; @@ -62,8 +103,8 @@ class GraphQLExplorer extends PureComponent { _navigateToSchema() { this.setState({ - currentType: null, - currentField: null, + currentType: undefined, + currentField: undefined, history: this._addToHistory(), }); } @@ -71,7 +112,7 @@ class GraphQLExplorer extends PureComponent { _handleNavigateType(type: GraphQLType) { this.setState({ currentType: type, - currentField: null, + currentField: undefined, history: this._addToHistory(), }); } @@ -89,8 +130,8 @@ class GraphQLExplorer extends PureComponent { const last = history[history.length - 1] || null; return { history: history.slice(0, history.length - 1), - currentType: last ? last.currentType : null, - currentField: last ? last.currentField : null, + currentType: last ? last.currentType : undefined, + currentField: last ? last.currentField : undefined, }; }); } @@ -114,21 +155,16 @@ class GraphQLExplorer extends PureComponent { // eslint-disable-next-line camelcase UNSAFE_componentWillReceiveProps(nextProps: Props) { - if (!nextProps.reference) { + const { reference } = nextProps; + + if (!reference) { return; } - const { type, field } = nextProps.reference; - const { currentType, currentField } = this.state; + const { currentField, currentType } = this.state; + const { type, field } = getReferenceInfo(reference); - const compare = (a: T, b: U) => (!a && !b) || (a && b && a.name === b.name); - - // @ts-expect-error -- needs generic for `name` - const sameType = compare(currentType, type); - const sameField = compare(currentField, field); - const nothingChanged = sameType && sameField; - - if (nothingChanged) { + if (isSameFieldAndType(currentType, type, currentField, field)) { return; } @@ -136,8 +172,8 @@ class GraphQLExplorer extends PureComponent { this.setState({ history, - currentType: type || null, - currentField: field || null, + currentType: type, + currentField: field, }); } @@ -168,8 +204,11 @@ class GraphQLExplorer extends PureComponent { if (lastField) { name = lastField.name || 'Unknown'; } else if (lastType) { - // @ts-expect-error -- needs generic for `name` - name = lastType.name || 'Unknown'; + if (isNamedType(lastType)) { + name = lastType.name; + } else { + name = 'Unknown'; + } } else { return null; } @@ -271,8 +310,8 @@ class GraphQLExplorer extends PureComponent { } const fieldName = currentField ? currentField.name : null; - // @ts-expect-error -- needs generic for `name` - const typeName = currentType ? currentType.name : null; + + const typeName = isNamedType(currentType) ? currentType.name : null; const schemaName = schema ? 'Schema' : null; return ( diff --git a/packages/insomnia-app/app/ui/components/graph-ql-explorer/graph-ql-types.ts b/packages/insomnia-app/app/ui/components/graph-ql-explorer/graph-ql-types.ts index 976c624bb0..468f469962 100644 --- a/packages/insomnia-app/app/ui/components/graph-ql-explorer/graph-ql-types.ts +++ b/packages/insomnia-app/app/ui/components/graph-ql-explorer/graph-ql-types.ts @@ -1,4 +1,5 @@ -import { GraphQLArgument, GraphQLField, GraphQLType } from 'graphql'; +import { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; +import { GraphQLField } from 'graphql'; type GraphQLFieldAny = GraphQLField; @@ -11,8 +12,4 @@ export interface GraphQLFieldWithParentName extends GraphQLFieldWithOptionalArgs parentName?: string; } -export interface ActiveReference { - type: GraphQLType | null; - argument: GraphQLArgument | null; - field: GraphQLFieldWithOptionalArgs | null; -} +export type ActiveReference = SchemaReference; diff --git a/packages/insomnia-app/app/ui/components/key-value-editor/row.tsx b/packages/insomnia-app/app/ui/components/key-value-editor/row.tsx index 77358fe261..aba94c99d1 100644 --- a/packages/insomnia-app/app/ui/components/key-value-editor/row.tsx +++ b/packages/insomnia-app/app/ui/components/key-value-editor/row.tsx @@ -212,7 +212,7 @@ class KeyValueEditorRow extends PureComponent { } } - _handleBlurDescription(e) { + _handleBlurDescription(e: FocusEvent) { if (this.props.onBlurDescription) { this.props.onBlurDescription(this.props.pair, e); } @@ -289,7 +289,6 @@ class KeyValueEditorRow extends PureComponent { > { return ( { this._valueInput = ref; }} - // @ts-expect-error -- TSCONVERSION very strange that one of the `OneLineEditor`s in this file _doesn't_ error with this... readOnly={readOnly} forceInput={forceInput} type={valueInputType || 'text'} @@ -492,7 +490,6 @@ class KeyValueEditorRow extends PureComponent { isVariableUncovered={isVariableUncovered} getAutocompleteConstants={this._handleAutocompleteNames} forceInput={forceInput} - // @ts-expect-error -- TSCONVERSION very strange that one of the `OneLineEditor`s in this file _doesn't_ error with this... readOnly={readOnly} onBlur={this._handleBlurName} onChange={this._handleNameChange} diff --git a/packages/insomnia-app/app/ui/components/markdown-editor.tsx b/packages/insomnia-app/app/ui/components/markdown-editor.tsx index 5ddd843fb7..543441ba9c 100644 --- a/packages/insomnia-app/app/ui/components/markdown-editor.tsx +++ b/packages/insomnia-app/app/ui/components/markdown-editor.tsx @@ -16,8 +16,8 @@ interface Props { indentSize: number; keyMap: string; lineWrapping: boolean; - handleRender: HandleRender; - handleGetRenderContext: HandleGetRenderContext; + handleRender?: HandleRender; + handleGetRenderContext?: HandleGetRenderContext; nunjucksPowerUserMode: boolean; isVariableUncovered: boolean; placeholder?: string; diff --git a/packages/insomnia-app/app/ui/components/modals/code-prompt-modal.tsx b/packages/insomnia-app/app/ui/components/modals/code-prompt-modal.tsx index 412373c4ea..b9f660deaf 100644 --- a/packages/insomnia-app/app/ui/components/modals/code-prompt-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/code-prompt-modal.tsx @@ -167,10 +167,8 @@ class CodePromptModal extends PureComponent { defaultValue={defaultValue} placeholder={placeholder} onChange={this._handleChange} - // @ts-expect-error -- TSCONVERSION appears to be a genuine error - handleGetRenderContext={enableRender ? handleGetRenderContext : null} - // @ts-expect-error -- TSCONVERSION appears to be a genuine error - handleRender={enableRender ? handleRender : null} + handleGetRenderContext={enableRender ? handleGetRenderContext : undefined} + handleRender={enableRender ? handleRender : undefined} mode={mode} keyMap={editorKeyMap} indentSize={editorIndentSize} @@ -192,10 +190,8 @@ class CodePromptModal extends PureComponent { onChange={this._handleChange} nunjucksPowerUserMode={nunjucksPowerUserMode} isVariableUncovered={isVariableUncovered} - // @ts-expect-error -- TSCONVERSION appears to be a genuine error - getRenderContext={enableRender ? handleGetRenderContext : null} - // @ts-expect-error -- TSCONVERSION appears to be a genuine error - render={enableRender ? handleRender : null} + getRenderContext={handleGetRenderContext} + render={handleRender} mode={mode} keyMap={editorKeyMap} indentSize={editorIndentSize} diff --git a/packages/insomnia-app/app/ui/components/modals/generate-code-modal.tsx b/packages/insomnia-app/app/ui/components/modals/generate-code-modal.tsx index 374413f173..14b075cb29 100644 --- a/packages/insomnia-app/app/ui/components/modals/generate-code-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/generate-code-modal.tsx @@ -1,9 +1,10 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; -import HTTPSnippet, { availableTargets } from 'httpsnippet'; +import HTTPSnippet, { HTTPSnippetClient, HTTPSnippetTarget } from 'httpsnippet'; import React, { PureComponent } from 'react'; import { AUTOBIND_CFG } from '../../../common/constants'; import { exportHarRequest } from '../../../common/har'; +import { Request } from '../../../models/request'; import { CopyButton } from '../base/copy-button'; import { Dropdown, DropdownButton, DropdownItem } from '../base/dropdown'; import Link from '../base/link'; @@ -13,8 +14,8 @@ import ModalFooter from '../base/modal-footer'; import ModalHeader from '../base/modal-header'; import CodeEditor from '../codemirror/code-editor'; -const DEFAULT_TARGET = availableTargets().find(t => t.key === 'shell'); -const DEFAULT_CLIENT = DEFAULT_TARGET.clients.find(t => t.key === 'curl'); +const DEFAULT_TARGET = HTTPSnippet.availableTargets().find(t => t.key === 'shell') as HTTPSnippetTarget; +const DEFAULT_CLIENT = DEFAULT_TARGET?.clients.find(t => t.key === 'curl') as HTTPSnippetClient; const MODE_MAP = { c: 'clike', java: 'clike', @@ -36,32 +37,30 @@ interface Props { interface State { cmd: string; - request: null; - target: string; - client: string; + request?: Request; + target: HTTPSnippetTarget; + client: HTTPSnippetClient; } @autoBindMethodsForReact(AUTOBIND_CFG) class GenerateCodeModal extends PureComponent { constructor(props: Props) { super(props); - let client; - let target; + let target: HTTPSnippetTarget | undefined; + let client: HTTPSnippetClient | undefined; // Load preferences from localStorage try { - // @ts-expect-error -- TSCONVERSION - target = JSON.parse(window.localStorage.getItem('insomnia::generateCode::target')); + target = JSON.parse(window.localStorage.getItem('insomnia::generateCode::target') || '') as HTTPSnippetTarget; } catch (e) {} try { - // @ts-expect-error -- TSCONVERSION - client = JSON.parse(window.localStorage.getItem('insomnia::generateCode::client')); + client = JSON.parse(window.localStorage.getItem('insomnia::generateCode::client') || '') as HTTPSnippetClient; } catch (e) {} this.state = { cmd: '', - request: null, + request: undefined, target: target || DEFAULT_TARGET, client: client || DEFAULT_CLIENT, }; @@ -85,13 +84,15 @@ class GenerateCodeModal extends PureComponent { _handleClientChange(client) { const { target, request } = this.state; + if (!request) { + return; + } this._generateCode(request, target, client); } _handleTargetChange(target) { - const { target: currentTarget } = this.state; + const { target: currentTarget, request } = this.state; - // @ts-expect-error -- TSCONVERSION if (currentTarget.key === target.key) { // No change return; @@ -99,16 +100,21 @@ class GenerateCodeModal extends PureComponent { const client = target.clients.find(c => c.key === target.default); - this._generateCode(this.state.request, target, client); + if (!request) { + return; + } + this._generateCode(request, target, client); } - async _generateCode(request, target, client) { + async _generateCode(request: Request, target: HTTPSnippetTarget, client: HTTPSnippetClient) { // Some clients need a content-length for the request to succeed const addContentLength = (TO_ADD_CONTENT_LENGTH[target.key] || []).find(c => c === client.key); const { environmentId } = this.props; const har = await exportHarRequest(request._id, environmentId, addContentLength); + // @TODO Should we throw instead? + if (!har) return; const snippet = new HTTPSnippet(har); - const cmd = snippet.convert(target.key, client.key); + const cmd = snippet.convert(target.key, client.key) || ''; this.setState({ request, cmd, @@ -120,7 +126,7 @@ class GenerateCodeModal extends PureComponent { window.localStorage.setItem('insomnia::generateCode::target', JSON.stringify(target)); } - show(request) { + show(request: Request) { const { client, target } = this.state; this._generateCode(request, target, client); @@ -131,13 +137,11 @@ class GenerateCodeModal extends PureComponent { render() { const { cmd, target, client } = this.state; const { editorFontSize, editorIndentSize, editorKeyMap } = this.props; - const targets = availableTargets(); + const targets = HTTPSnippet.availableTargets(); // NOTE: Just some extra precautions in case the target is messed up - let clients = []; + let clients: HTTPSnippetClient[] = []; - // @ts-expect-error -- TSCONVERSION if (target && Array.isArray(target.clients)) { - // @ts-expect-error -- TSCONVERSION clients = target.clients; } @@ -156,7 +160,6 @@ class GenerateCodeModal extends PureComponent { { - // @ts-expect-error -- TSCONVERSION target ? target.title : 'n/a' } @@ -170,24 +173,16 @@ class GenerateCodeModal extends PureComponent {    - { - - // @ts-expect-error -- TSCONVERSION - client ? client.title : 'n/a' - } + {client ? client.title : 'n/a'} {clients.map(client => ( - { - // @ts-expect-error -- TSCONVERSION - client.title - } + {client.title} ))} @@ -199,7 +194,6 @@ class GenerateCodeModal extends PureComponent { placeholder="Generating code snippet..." className="border-top" key={Date.now()} - // @ts-expect-error -- TSCONVERSION mode={MODE_MAP[target.key] || target.key} ref={this._setEditorRef} fontSize={editorFontSize} diff --git a/packages/insomnia-app/app/ui/components/modals/generate-config-modal.tsx b/packages/insomnia-app/app/ui/components/modals/generate-config-modal.tsx index f19bf2168e..a718697cc1 100644 --- a/packages/insomnia-app/app/ui/components/modals/generate-config-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/generate-config-modal.tsx @@ -112,8 +112,6 @@ class GenerateConfigModal extends PureComponent { lineWrapping={settings.editorLineWrapping} mode={config.mimeType} nunjucksPowerUserMode={settings.nunjucksPowerUserMode} - // @ts-expect-error -- TSCONVERSION appears to be genuine - onChange={this._handleChange} readOnly /> diff --git a/packages/insomnia-app/app/ui/components/request-url-bar.tsx b/packages/insomnia-app/app/ui/components/request-url-bar.tsx index 170f42af8d..4cbbf0ef94 100644 --- a/packages/insomnia-app/app/ui/components/request-url-bar.tsx +++ b/packages/insomnia-app/app/ui/components/request-url-bar.tsx @@ -22,7 +22,7 @@ import KeydownBinder from './keydown-binder'; import { showPrompt } from './modals/index'; interface Props { - handleAutocompleteUrls: Function; + handleAutocompleteUrls: () => Promise; handleGenerateCode: Function; handleGetRenderContext: HandleGetRenderContext; handleImport: Function; @@ -58,7 +58,7 @@ class RequestUrlBar extends PureComponent { currentTimeout: null, }; - _lastPastedText: string | null = null; + _lastPastedText?: string; _setDropdownRef(n: Dropdown) { this._dropdown = n; @@ -102,7 +102,7 @@ class RequestUrlBar extends PureComponent { } // Reset pasted text cache - this._lastPastedText = null; + this._lastPastedText = undefined; // Attempt to import the pasted text const importedRequest = await this.props.handleImport(pastedText); @@ -113,9 +113,9 @@ class RequestUrlBar extends PureComponent { }, DEBOUNCE_MILLIS); } - _handleUrlPaste(e: React.ClipboardEvent) { + _handleUrlPaste(e: ClipboardEvent) { // NOTE: We're not actually doing the import here to avoid races with onChange - this._lastPastedText = e.clipboardData.getData('text/plain'); + this._lastPastedText = e.clipboardData?.getData('text/plain'); } _handleGenerateCode() { diff --git a/packages/insomnia-app/app/ui/components/wrapper-unit-test.tsx b/packages/insomnia-app/app/ui/components/wrapper-unit-test.tsx index 6d936bdaee..854be25a81 100644 --- a/packages/insomnia-app/app/ui/components/wrapper-unit-test.tsx +++ b/packages/insomnia-app/app/ui/components/wrapper-unit-test.tsx @@ -437,7 +437,6 @@ class WrapperUnitTest extends PureComponent { lintOptions={WrapperUnitTest.lintOptions} onChange={this._handleUnitTestCodeChange.bind(this, unitTest)} nunjucksPowerUserMode={settings.nunjucksPowerUserMode} - // @ts-expect-error -- TSCONVERSION isVariableUncovered={settings.isVariableUncovered} mode="javascript" lineWrapping={settings.editorLineWrapping} @@ -473,8 +472,7 @@ class WrapperUnitTest extends PureComponent { bg="surprise" onClick={this._handleRunTests} size="default" - // @ts-expect-error -- TSCONVERSION - disabled={testsRunning} + disabled={Boolean(testsRunning)} > {testsRunning ? 'Running... ' : 'Run Tests'} @@ -519,8 +517,7 @@ class WrapperUnitTest extends PureComponent { {testsRunning ? 'Running... ' : 'Run Tests'} diff --git a/packages/insomnia-app/app/ui/containers/app.tsx b/packages/insomnia-app/app/ui/containers/app.tsx index 302deadcc4..7a29b82415 100644 --- a/packages/insomnia-app/app/ui/containers/app.tsx +++ b/packages/insomnia-app/app/ui/containers/app.tsx @@ -557,7 +557,11 @@ class App extends PureComponent { const har = await exportHarRequest(request._id, environmentId); const snippet = new HTTPSnippet(har); const cmd = snippet.convert('shell', 'curl'); - clipboard.writeText(cmd); + + // @TODO Should we throw otherwise? What should happen if we cannot find cmd? + if (cmd) { + clipboard.writeText(cmd); + } } static async _updateRequestGroupMetaByParentId(requestGroupId, patch) { diff --git a/packages/insomnia-app/package-lock.json b/packages/insomnia-app/package-lock.json index 90ca74cfff..2fe3790036 100644 --- a/packages/insomnia-app/package-lock.json +++ b/packages/insomnia-app/package-lock.json @@ -2225,9 +2225,9 @@ "dev": true }, "@types/codemirror": { - "version": "5.60.0", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.0.tgz", - "integrity": "sha512-xgzXZyCzedLRNC67/Nn8rpBtTFnAsX2C+Q/LGoH6zgcpF/LqdNHJMHEOhqT1bwUcSp6kQdOIuKzRbeW9DYhEhg==", + "version": "5.60.2", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.2.tgz", + "integrity": "sha512-tk8YxckrdU49GaJYRKxdzzzXrTlyT2nQGnobb8rAk34jt+kYXOxPKGqNgr7SJpl5r6YGaRD4CDfqiL+6A+/z7w==", "dev": true, "requires": { "@types/tern": "*" @@ -2810,9 +2810,9 @@ "dev": true }, "@types/tern": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.3.tgz", - "integrity": "sha512-imDtS4TAoTcXk0g7u4kkWqedB3E4qpjXzCpD2LU5M5NAXHzCDsypyvXSaG7mM8DKYkCRa7tFp4tS/lp/Wo7Q3w==", + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz", + "integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==", "dev": true, "requires": { "@types/estree": "*" @@ -5696,14 +5696,14 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "codemirror": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.0.tgz", - "integrity": "sha512-Xnl3304iCc8nyVZuRkzDVVwc794uc9QNX0UcPGeNic1fbzkSrO4l4GVXho9tRNKBgPYZXgocUqXyfIv3BILhCQ==" + "version": "5.62.3", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.3.tgz", + "integrity": "sha512-zZAyOfN8TU67ngqrxhOgtkSAGV9jSpN1snbl8elPtnh9Z5A11daR405+dhLzLnuXrwX0WCShWlybxPN3QC/9Pg==" }, "codemirror-graphql": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-1.0.1.tgz", - "integrity": "sha512-5ttMpv2kMn99Rmf2aZ5P6/hMd3y11cN8LP/x5MUeF0ipcalZA/GE/OxxXkhV0YJE/uW5QIcPyZDkvtSsGZa23A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-1.0.2.tgz", + "integrity": "sha512-D4+BdYa6iQnDlio4mBk1Yap5ROCqEWapSFLkiKGatx/I0dF6euzdwd0um3Ndudw6rFQbNuT7hpcH8tnBO6VOfQ==", "requires": { "graphql-language-service-interface": "^2.8.2", "graphql-language-service-parser": "^1.9.0" @@ -9728,9 +9728,9 @@ } }, "graphql-language-service-interface": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.3.tgz", - "integrity": "sha512-Gh4Q3dlCT1MrZGO0eaz7v31gkp8fh+ig94YH/A+1Th2q+k3RsRqfSJm5tKZ8TJ4rSADZ/dj+hzOpWCGzLyCiHQ==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.4.tgz", + "integrity": "sha512-myW8z7HOZkYfhYGKDc0URFkTZChp41Po890W92zuBIhGccckgtiWSJGXaLX+r9QAwVIeZhKaNgEacsyvQb1f/g==", "requires": { "graphql-language-service-parser": "^1.9.0", "graphql-language-service-types": "^1.8.0", @@ -9739,22 +9739,22 @@ } }, "graphql-language-service-parser": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.1.tgz", - "integrity": "sha512-GySsDrYxzxu6r1vF282xXDR2KlfVL5aOW7pgc75fF3UFiuqGm/SeoIljNM0mLpRl5KSxo1HNOxhkWoFBoy/h2w==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.2.tgz", + "integrity": "sha512-3txms73cJsXDfJQdR5hI83N2rpTuq9FD6aijdrXAeSuI5B60g32DxjelUkt4Ge+2BvBEDLn5ppXlpVYDC9UQHQ==", "requires": { "graphql-language-service-types": "^1.8.0" } }, "graphql-language-service-types": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.1.tgz", - "integrity": "sha512-IpYS0mEHEmRsFlq+loWCpSYYYizAID7Alri6GoFN1QqUdux+8rp1Tkp2NGsGDpDmm3Dbz5ojmJWzNWQGpuwveA==" + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.2.tgz", + "integrity": "sha512-Sj07RHnMwAhEvAt7Jdt1l/x56ZpoNh+V6g+T58CF6GiYqI5l4vXqqRB4d4xHDcNQX98GpJfnf3o8BqPgP3C5Sw==" }, "graphql-language-service-utils": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.5.2.tgz", - "integrity": "sha512-hXGd4ARhyD7WTmTwuYmCYo6BcY8FtTp+1JHLaUG0Q63k0NpZTuFuRZ+N7TSP9mcRb7labeozs3DYgaqStsDe1A==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.5.3.tgz", + "integrity": "sha512-ydevEZ0AgzEKQF3hiCbLXuS0o7189Ww/T30WtCKCLaRHDYk9Yyb2PZWdhSTWLxYZTaX2TccV6NtFWvzIC7UP3g==", "requires": { "graphql-language-service-types": "^1.8.0", "nullthrows": "^1.0.0" diff --git a/packages/insomnia-app/package.json b/packages/insomnia-app/package.json index 9f751b2c60..3ab82de2a4 100644 --- a/packages/insomnia-app/package.json +++ b/packages/insomnia-app/package.json @@ -83,8 +83,8 @@ "class-autobind-decorator": "^3.0.1", "classnames": "^2.3.1", "clone": "^2.1.0", - "codemirror": "^5.62.0", - "codemirror-graphql": "^1.0.1", + "codemirror": "^5.62.3", + "codemirror-graphql": "^1.0.2", "color": "^3.1.2", "deep-equal": "^1.0.1", "electron-context-menu": "^2.2.0", @@ -180,7 +180,7 @@ "@types/analytics-node": "^3.1.4", "@types/aws4": "^1.5.1", "@types/clone": "^2.1.0", - "@types/codemirror": "^5.60.0", + "@types/codemirror": "^5.60.2", "@types/color": "^3.0.1", "@types/concurrently": "^6.0.1", "@types/deep-equal": "^1.0.1",