From 3eedddc0739b324aed5a1254eff3528c97ceba75 Mon Sep 17 00:00:00 2001 From: Jack Kavanagh Date: Mon, 14 Aug 2023 17:11:41 +0200 Subject: [PATCH] autocomplete revision (#6220) * first pass * cut copy paste * scope only to code editors * nested tree menu * organise context menu * remove menu item * working insert * fix spelling * use id to filter right click event * add ids to all code editors * handle all tags * sort alphabetically * remove console logs --- packages/insomnia/src/main/ipc/electron.ts | 70 ++++++++++++++++++- packages/insomnia/src/main/ipc/main.ts | 1 + packages/insomnia/src/preload.ts | 1 + .../ui/components/codemirror/code-editor.tsx | 12 +++- .../components/codemirror/one-line-editor.tsx | 12 +++- .../editors/body/graph-ql-editor.tsx | 2 + .../ui/components/editors/body/raw-editor.tsx | 1 + .../components/editors/environment-editor.tsx | 1 + .../editors/request-headers-editor.tsx | 1 + .../editors/request-parameters-editor.tsx | 1 + .../ui/components/key-value-editor/row.tsx | 10 +-- .../src/ui/components/markdown-editor.tsx | 1 + .../components/modals/code-prompt-modal.tsx | 1 + .../components/modals/cookie-modify-modal.tsx | 4 ++ .../components/modals/generate-code-modal.tsx | 1 + .../modals/generate-config-modal.tsx | 63 +++++++++-------- .../ui/components/panes/grpc-request-pane.tsx | 3 + .../components/panes/grpc-response-pane.tsx | 1 + .../viewers/response-timeline-viewer.tsx | 1 + .../ui/components/viewers/response-viewer.tsx | 2 + .../ui/components/websockets/action-bar.tsx | 1 + .../ui/components/websockets/event-view.tsx | 32 +++------ .../websockets/websocket-request-pane.tsx | 1 + packages/insomnia/src/ui/routes/design.tsx | 1 + .../insomnia/src/ui/routes/test-suite.tsx | 1 + 25 files changed, 160 insertions(+), 65 deletions(-) diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index 036c99a93d..771434633f 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -1,11 +1,75 @@ -import type { OpenDialogOptions, SaveDialogOptions } from 'electron'; -import { app, BrowserWindow, clipboard, dialog, ipcMain, shell } from 'electron'; +import type { MenuItemConstructorOptions, OpenDialogOptions, SaveDialogOptions } from 'electron'; +import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, shell } from 'electron'; +import { fnOrString } from '../../common/misc'; +import type { NunjucksParsedTagArg } from '../../templating/utils'; +import { localTemplateTags } from '../../ui/components/templating/local-template-tags'; +import { invariant } from '../../utils/invariant'; + +const getTemplateValue = (arg: NunjucksParsedTagArg) => { + if (arg.defaultValue === undefined) { + return "''"; + } + if (typeof arg.defaultValue === 'string') { + return `'${arg.defaultValue}'`; + } + return arg.defaultValue; +}; export function registerElectronHandlers() { + ipcMain.on('show-context-menu', (event, options) => { + try { + const template: MenuItemConstructorOptions[] = [ + { + role: 'cut', + }, + { + role: 'copy', + }, + { + role: 'paste', + }, + { type: 'separator' }, + ...localTemplateTags + // sort alphabetically + .sort((a, b) => fnOrString(a.templateTag.displayName).localeCompare(fnOrString(b.templateTag.displayName))) + .map(l => { + const actions = l.templateTag.args?.[0]; + const otherArgs = l.templateTag.args?.slice(1); + const hasSubmenu = actions?.options?.length; + return { + label: fnOrString(l.templateTag.displayName), + ...(hasSubmenu ? {} : { + click: () => { + const tag = `{% ${l.templateTag.name} ${l.templateTag.args?.map(getTemplateValue).join(', ')} %}`; + event.sender.send('context-menu-command', { key: options.key, tag }); + }, + }), + ...(hasSubmenu ? { + submenu: actions?.options?.map(action => ({ + label: fnOrString(action.displayName), + click: () => { + const defaultTagArgs = otherArgs ? ', ' + otherArgs.map(getTemplateValue).join(', ') : ''; + const tag = `{% ${l.templateTag.name} '${action.value}'${defaultTagArgs} %}`; + event.sender.send('context-menu-command', { key: options.key, tag }); + }, + })), + } : {}), + }; + }), + + ]; + const menu = Menu.buildFromTemplate(template); + const win = BrowserWindow.fromWebContents(event.sender); + invariant(win, 'expected window'); + menu.popup({ window: win }); + } catch (e) { + console.error(e); + } + }); ipcMain.on('setMenuBarVisibility', (_, visible: boolean) => { BrowserWindow.getAllWindows() .forEach(window => { - // the `setMenuBarVisibility` signature uses `visible` semantics + // the `setMenuBarVisibility` signature uses `visible` semantics window.setMenuBarVisibility(visible); // the `setAutoHideMenu` signature uses `hide` semantics const hide = !visible; diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index e2d29db67e..537b60b4b4 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -40,6 +40,7 @@ export interface MainBridgeAPI { trackPageView: (options: { name: string }) => void; axiosRequest: typeof axiosRequest; insomniaFetch: typeof insomniaFetch; + showContextMenu: (options: { key: string }) => void; } export function registerMainHandlers() { ipcMain.handle('insomniaFetch', async (_, options: Parameters[0]) => { diff --git a/packages/insomnia/src/preload.ts b/packages/insomnia/src/preload.ts index 326d49d39f..e2ce67ff81 100644 --- a/packages/insomnia/src/preload.ts +++ b/packages/insomnia/src/preload.ts @@ -62,6 +62,7 @@ const main: Window['main'] = { trackPageView: options => ipcRenderer.send('trackPageView', options), axiosRequest: options => ipcRenderer.invoke('axiosRequest', options), insomniaFetch: options => ipcRenderer.invoke('insomniaFetch', options), + showContextMenu: options => ipcRenderer.send('show-context-menu', options), }; const dialog: Window['dialog'] = { showOpenDialog: options => ipcRenderer.invoke('showOpenDialog', options), diff --git a/packages/insomnia/src/ui/components/codemirror/code-editor.tsx b/packages/insomnia/src/ui/components/codemirror/code-editor.tsx index 8d9ff5549d..8d7665cf5f 100644 --- a/packages/insomnia/src/ui/components/codemirror/code-editor.tsx +++ b/packages/insomnia/src/ui/components/codemirror/code-editor.tsx @@ -81,7 +81,7 @@ export interface CodeEditorProps { hideGutters?: boolean; hideLineNumbers?: boolean; hintOptions?: ShowHintOptions; - id?: string; + id: string; infoOptions?: GraphQLInfoOptions; jumpOptions?: ModifiedGraphQLJumpOptions; lintOptions?: Record; @@ -485,7 +485,8 @@ export const CodeEditor = forwardRef(({ console.log('Failed to set CodeMirror option', err.message, { key, value }); } }; - + useEffect(() => window.main.on('context-menu-command', (_, { key, tag }) => + id === key && codeMirror.current?.replaceSelection(tag)), [id]); useEffect(() => tryToSetOption('hintOptions', hintOptions), [hintOptions]); useEffect(() => tryToSetOption('info', infoOptions), [infoOptions]); useEffect(() => tryToSetOption('jump', jumpOptions), [jumpOptions]); @@ -523,6 +524,13 @@ export const CodeEditor = forwardRef(({ style={style} data-editor-type="text" data-testid="CodeEditor" + onContextMenu={event => { + if (readOnly) { + return; + } + event.preventDefault(); + window.main.showContextMenu({ key: id }); + }} >
string[] | PromiseLike; - id?: string; + id: string; onChange: (value: string) => void; onKeyDown?: (event: KeyboardEvent, value: string) => void; onPaste?: (event: ClipboardEvent) => void; @@ -194,6 +194,9 @@ export const OneLineEditor = forwardRef return () => codeMirror.current?.on('paste', handlePaste); }, [onPaste]); + useEffect(() => window.main.on('context-menu-command', (_, { key, tag }) => + id === key && codeMirror.current?.replaceSelection(tag)), [id]); + useImperativeHandle(ref, () => ({ selectAll: () => codeMirror.current?.setSelection({ line: 0, ch: 0 }, { line: codeMirror.current.lineCount(), ch: 0 }), focusEnd: () => { @@ -212,6 +215,13 @@ export const OneLineEditor = forwardRef })} data-editor-type={type || 'text'} data-testid="OneLineEditor" + onContextMenu={event => { + if (readOnly) { + return; + } + event.preventDefault(); + window.main.showContextMenu({ key: id }); + }} >