mirror of
https://github.com/Kong/insomnia.git
synced 2026-05-24 08:37:35 -04:00
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
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<typeof insomniaFetch>[0]) => {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -81,7 +81,7 @@ export interface CodeEditorProps {
|
||||
hideGutters?: boolean;
|
||||
hideLineNumbers?: boolean;
|
||||
hintOptions?: ShowHintOptions;
|
||||
id?: string;
|
||||
id: string;
|
||||
infoOptions?: GraphQLInfoOptions;
|
||||
jumpOptions?: ModifiedGraphQLJumpOptions;
|
||||
lintOptions?: Record<string, any>;
|
||||
@@ -485,7 +485,8 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({
|
||||
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<CodeEditorHandle, CodeEditorProps>(({
|
||||
style={style}
|
||||
data-editor-type="text"
|
||||
data-testid="CodeEditor"
|
||||
onContextMenu={event => {
|
||||
if (readOnly) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
window.main.showContextMenu({ key: id });
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classnames('editor__container', 'input', className)}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { isKeyCombinationInRegistry } from '../settings/shortcuts';
|
||||
export interface OneLineEditorProps {
|
||||
defaultValue: string;
|
||||
getAutocompleteConstants?: () => string[] | PromiseLike<string[]>;
|
||||
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<OneLineEditorHandle, OneLineEditorProps>
|
||||
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<OneLineEditorHandle, OneLineEditorProps>
|
||||
})}
|
||||
data-editor-type={type || 'text'}
|
||||
data-testid="OneLineEditor"
|
||||
onContextMenu={event => {
|
||||
if (readOnly) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
window.main.showContextMenu({ key: id });
|
||||
}}
|
||||
>
|
||||
<div className="editor__container input editor--single-line">
|
||||
<textarea
|
||||
|
||||
@@ -581,6 +581,7 @@ export const GraphQLEditor: FC<Props> = ({
|
||||
|
||||
<div className="graphql-editor__query">
|
||||
<CodeEditor
|
||||
id="graphql-editor"
|
||||
ref={editorRef}
|
||||
dynamicHeight
|
||||
showPrettifyButton
|
||||
@@ -627,6 +628,7 @@ export const GraphQLEditor: FC<Props> = ({
|
||||
</h2>
|
||||
<div className="graphql-editor__variables">
|
||||
<CodeEditor
|
||||
id="graphql-editor-variables"
|
||||
dynamicHeight
|
||||
enableNunjucks
|
||||
uniquenessKey={uniquenessKey ? uniquenessKey + '::variables' : undefined}
|
||||
|
||||
@@ -19,6 +19,7 @@ export const RawEditor: FC<Props> = ({
|
||||
}) => (
|
||||
<Fragment>
|
||||
<CodeEditor
|
||||
id="raw-editor"
|
||||
showPrettifyButton
|
||||
uniquenessKey={uniquenessKey}
|
||||
defaultValue={content}
|
||||
|
||||
@@ -101,6 +101,7 @@ export const EnvironmentEditor = forwardRef<EnvironmentEditorHandle, Props>(({
|
||||
return (
|
||||
<div className="environment-editor">
|
||||
<CodeEditor
|
||||
id="environment-editor"
|
||||
ref={editorRef}
|
||||
autoPrettify
|
||||
enableNunjucks
|
||||
|
||||
@@ -68,6 +68,7 @@ export const RequestHeadersEditor: FC<Props> = ({
|
||||
return (
|
||||
<div className="tall">
|
||||
<CodeEditor
|
||||
id="request-headers-editor"
|
||||
onChange={handleBulkUpdate}
|
||||
defaultValue={headersString}
|
||||
enableNunjucks
|
||||
|
||||
@@ -64,6 +64,7 @@ export const RequestParametersEditor: FC<Props> = ({
|
||||
if (bulk) {
|
||||
return (
|
||||
<CodeEditor
|
||||
id="request-parameters-editor"
|
||||
onChange={handleBulkUpdate}
|
||||
defaultValue={paramsString}
|
||||
enableNunjucks
|
||||
|
||||
@@ -86,6 +86,7 @@ export const Row: FC<Props> = ({
|
||||
})}
|
||||
>
|
||||
<OneLineEditor
|
||||
id={'key-value-editor__name' + pair.id}
|
||||
placeholder={namePlaceholder || 'Name'}
|
||||
defaultValue={pair.name}
|
||||
getAutocompleteConstants={() => handleGetAutocompleteNameConstants?.(pair) || []}
|
||||
@@ -124,15 +125,16 @@ export const Row: FC<Props> = ({
|
||||
</button>
|
||||
) : (
|
||||
<OneLineEditor
|
||||
readOnly={readOnly}
|
||||
|
||||
id={'key-value-editor__value' + pair.id}
|
||||
type="text"
|
||||
readOnly={readOnly}
|
||||
placeholder={valuePlaceholder || 'Value'}
|
||||
defaultValue={pair.value}
|
||||
onChange={value => onChange({ ...pair, value })}
|
||||
getAutocompleteConstants={() => handleGetAutocompleteValueConstants?.(pair) || []}
|
||||
/>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{showDescription ? (
|
||||
<div
|
||||
@@ -142,8 +144,8 @@ export const Row: FC<Props> = ({
|
||||
)}
|
||||
>
|
||||
<OneLineEditor
|
||||
id={'key-value-editor__description' + pair.id}
|
||||
readOnly={readOnly}
|
||||
|
||||
placeholder={descriptionPlaceholder || 'Description'}
|
||||
defaultValue={pair.description || ''}
|
||||
onChange={description => onChange({ ...pair, description })}
|
||||
|
||||
@@ -80,6 +80,7 @@ export const MarkdownEditor = forwardRef<CodeEditorHandle, Props>(({
|
||||
<MarkdownEdit withDynamicHeight={!tall}>
|
||||
<div className='form-control form-control--outlined'>
|
||||
<CodeEditor
|
||||
id="markdown-editor"
|
||||
ref={ref}
|
||||
hideGutters
|
||||
hideLineNumbers
|
||||
|
||||
@@ -118,6 +118,7 @@ export const CodePromptModal = forwardRef<CodePromptModalHandle, ModalProps>((_,
|
||||
<div className="pad-sm pad-bottom tall">
|
||||
<div className="form-control form-control--outlined form-control--tall tall">
|
||||
<CodeEditor
|
||||
id="code-prompt-modal"
|
||||
hideLineNumbers
|
||||
showPrettifyButton
|
||||
className="tall"
|
||||
|
||||
@@ -89,6 +89,7 @@ export const CookieModifyModal = ((props: ModalProps & CookieModifyModalOptions)
|
||||
<label data-testid="CookieKey">
|
||||
Key
|
||||
<OneLineEditor
|
||||
id="cookie-key"
|
||||
defaultValue={(cookie && cookie.key || '').toString()}
|
||||
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { key: value.trim() }))}
|
||||
/>
|
||||
@@ -98,6 +99,7 @@ export const CookieModifyModal = ((props: ModalProps & CookieModifyModalOptions)
|
||||
<label data-testid="CookieValue">
|
||||
Value
|
||||
<OneLineEditor
|
||||
id="cookie-value"
|
||||
defaultValue={(cookie && cookie.value || '').toString()}
|
||||
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { value: value.trim() }))}
|
||||
/>
|
||||
@@ -109,6 +111,7 @@ export const CookieModifyModal = ((props: ModalProps & CookieModifyModalOptions)
|
||||
<label data-testid="CookieDomain">
|
||||
Domain
|
||||
<OneLineEditor
|
||||
id="cookie-domain"
|
||||
defaultValue={(cookie && cookie.domain || '').toString()}
|
||||
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { domain: value.trim() }))}
|
||||
/>
|
||||
@@ -118,6 +121,7 @@ export const CookieModifyModal = ((props: ModalProps & CookieModifyModalOptions)
|
||||
<label data-testid="CookiePath">
|
||||
Path
|
||||
<OneLineEditor
|
||||
id="cookie-path"
|
||||
defaultValue={(cookie && cookie.path || '').toString()}
|
||||
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { path: value.trim() }))}
|
||||
/>
|
||||
|
||||
@@ -173,6 +173,7 @@ export const GenerateCodeModal = forwardRef<GenerateCodeModalHandle, Props>((pro
|
||||
<CopyButton content={cmd} className="pull-right" />
|
||||
</div>
|
||||
{target && <CodeEditor
|
||||
id="generate-code-modal-content"
|
||||
placeholder="Generating code snippet..."
|
||||
className="border-top"
|
||||
key={Date.now()}
|
||||
|
||||
@@ -147,37 +147,38 @@ export const GenerateConfigModal = forwardRef<GenerateConfigModalHandle, ModalPr
|
||||
onSelectionChange={onSelect}
|
||||
>
|
||||
{configs.map(config =>
|
||||
(<TabItem
|
||||
key={config.label}
|
||||
title={
|
||||
<>
|
||||
{config.label}
|
||||
{config.docsLink ?
|
||||
<>
|
||||
{' '}
|
||||
<HelpTooltip>
|
||||
To learn more about {config.label}
|
||||
<br />
|
||||
<Link href={config.docsLink}>Documentation {<i className="fa fa-external-link-square" />}</Link>
|
||||
</HelpTooltip>
|
||||
</> : null}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PanelContainer key={config.label}>
|
||||
{config.error ?
|
||||
<p className="notice error margin-md">
|
||||
{config.error}
|
||||
{config.docsLink ? <><br /><Link href={config.docsLink}>Documentation {<i className="fa fa-external-link-square" />}</Link></> : null}
|
||||
</p> :
|
||||
<CodeEditor
|
||||
className="tall pad-top-sm"
|
||||
defaultValue={config.content}
|
||||
mode={config.mimeType}
|
||||
readOnly
|
||||
/>}
|
||||
</PanelContainer>
|
||||
</TabItem>)
|
||||
(<TabItem
|
||||
key={config.label}
|
||||
title={
|
||||
<>
|
||||
{config.label}
|
||||
{config.docsLink ?
|
||||
<>
|
||||
{' '}
|
||||
<HelpTooltip>
|
||||
To learn more about {config.label}
|
||||
<br />
|
||||
<Link href={config.docsLink}>Documentation {<i className="fa fa-external-link-square" />}</Link>
|
||||
</HelpTooltip>
|
||||
</> : null}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PanelContainer key={config.label}>
|
||||
{config.error ?
|
||||
<p className="notice error margin-md">
|
||||
{config.error}
|
||||
{config.docsLink ? <><br /><Link href={config.docsLink}>Documentation {<i className="fa fa-external-link-square" />}</Link></> : null}
|
||||
</p> :
|
||||
<CodeEditor
|
||||
id="generate-config-modal"
|
||||
className="tall pad-top-sm"
|
||||
defaultValue={config.content}
|
||||
mode={config.mimeType}
|
||||
readOnly
|
||||
/>}
|
||||
</PanelContainer>
|
||||
</TabItem>)
|
||||
)}
|
||||
</Tabs>
|
||||
</ModalBody>
|
||||
|
||||
@@ -152,6 +152,7 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
|
||||
<div className="method-grpc pad-right pad-left vertically-center">gRPC</div>
|
||||
<StyledUrlEditor title={activeRequest.url}>
|
||||
<OneLineEditor
|
||||
id="grpc-url"
|
||||
key={uniquenessKey}
|
||||
type="text"
|
||||
defaultValue={activeRequest.url}
|
||||
@@ -270,6 +271,7 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
|
||||
{[
|
||||
<TabItem key="body" title="Body">
|
||||
<CodeEditor
|
||||
id="grpc-request-editor"
|
||||
ref={editorRef}
|
||||
defaultValue={activeRequest.body.text}
|
||||
onChange={text => patchRequest(requestId, { body: { text } })}
|
||||
@@ -281,6 +283,7 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
|
||||
...requestMessages.sort((a, b) => a.created - b.created).map((m, index) => (
|
||||
<TabItem key={m.id} title={`Stream ${index + 1}`}>
|
||||
<CodeEditor
|
||||
id={'grpc-request-editor-tab' + m.id}
|
||||
defaultValue={m.text}
|
||||
mode="application/json"
|
||||
enableNunjucks
|
||||
|
||||
@@ -25,6 +25,7 @@ export const GrpcResponsePane: FunctionComponent<Props> = ({ grpcState: { runnin
|
||||
{responseMessages.sort((a, b) => a.created - b.created).map((m, index) => (
|
||||
<TabItem key={m.id} title={`Response ${index + 1}`}>
|
||||
<CodeEditor
|
||||
id="grpc-response"
|
||||
defaultValue={m.text}
|
||||
mode="application/json"
|
||||
enableNunjucks
|
||||
|
||||
@@ -43,6 +43,7 @@ export const ResponseTimelineViewer: FC<Props> = ({ timeline, pinToBottom }) =>
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
id="response-timeline-viewer"
|
||||
ref={editorRef}
|
||||
hideLineNumbers
|
||||
readOnly
|
||||
|
||||
@@ -330,6 +330,7 @@ export const ResponseViewer = ({
|
||||
if (previewMode === PREVIEW_MODE_RAW) {
|
||||
return (
|
||||
<CodeEditor
|
||||
id="raw-response-viewer"
|
||||
key={responseId}
|
||||
ref={editorRef}
|
||||
className="raw-editor"
|
||||
@@ -347,6 +348,7 @@ export const ResponseViewer = ({
|
||||
// Show everything else as "source"
|
||||
return (
|
||||
<CodeEditor
|
||||
id="response-viewer"
|
||||
key={disablePreviewLinks ? 'links-disabled' : 'links-enabled'}
|
||||
ref={editorRef}
|
||||
autoPrettify
|
||||
|
||||
@@ -136,6 +136,7 @@ export const WebSocketActionBar: FC<ActionBarProps> = ({ request, environmentId,
|
||||
>
|
||||
<StyledUrlBar>
|
||||
<OneLineEditor
|
||||
id="websocket-url-bar"
|
||||
ref={oneLineEditorRef}
|
||||
onKeyDown={createKeybindingsHandler({
|
||||
'Enter': () => handleSubmit(),
|
||||
|
||||
@@ -104,30 +104,14 @@ export const MessageEventView: FC<Props<CurlMessageEvent | WebSocketMessageEvent
|
||||
/>
|
||||
</PreviewPaneButtons>
|
||||
<PreviewPaneContents>
|
||||
{previewMode === PREVIEW_MODE_FRIENDLY &&
|
||||
<CodeEditor
|
||||
hideLineNumbers
|
||||
mode={'text/json'}
|
||||
defaultValue={pretty}
|
||||
uniquenessKey={event._id}
|
||||
readOnly
|
||||
/>}
|
||||
{previewMode === PREVIEW_MODE_SOURCE &&
|
||||
<CodeEditor
|
||||
hideLineNumbers
|
||||
mode={'text/json'}
|
||||
defaultValue={raw}
|
||||
uniquenessKey={event._id}
|
||||
readOnly
|
||||
/>}
|
||||
{previewMode === PREVIEW_MODE_RAW &&
|
||||
<CodeEditor
|
||||
hideLineNumbers
|
||||
mode={'text/plain'}
|
||||
defaultValue={raw}
|
||||
uniquenessKey={event._id}
|
||||
readOnly
|
||||
/>}
|
||||
<CodeEditor
|
||||
id="websocket-body-preview"
|
||||
hideLineNumbers
|
||||
mode={previewMode === PREVIEW_MODE_RAW ? 'text/plain' : 'text/json'}
|
||||
defaultValue={previewMode === PREVIEW_MODE_FRIENDLY ? pretty : raw}
|
||||
uniquenessKey={event._id}
|
||||
readOnly
|
||||
/>
|
||||
</PreviewPaneContents>
|
||||
</PreviewPane>
|
||||
);
|
||||
|
||||
@@ -185,6 +185,7 @@ const WebSocketRequestForm: FC<FormProps> = ({
|
||||
}}
|
||||
>
|
||||
<CodeEditor
|
||||
id="websocket-message-editor"
|
||||
showPrettifyButton
|
||||
uniquenessKey={request._id}
|
||||
mode={previewMode}
|
||||
|
||||
@@ -282,6 +282,7 @@ const Design: FC = () => {
|
||||
<div className="column tall theme--pane__body">
|
||||
<div className="tall relative overflow-hidden">
|
||||
<CodeEditor
|
||||
id="spec-editor"
|
||||
key={uniquenessKey}
|
||||
showPrettifyButton
|
||||
ref={editor}
|
||||
|
||||
@@ -132,6 +132,7 @@ const UnitTestItemView = ({
|
||||
}
|
||||
>
|
||||
<CodeEditor
|
||||
id="unit-test-editor"
|
||||
ref={editorRef}
|
||||
dynamicHeight
|
||||
showPrettifyButton
|
||||
|
||||
Reference in New Issue
Block a user