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:
Jack Kavanagh
2023-08-14 17:11:41 +02:00
committed by GitHub
parent c4728cb5e8
commit 3eedddc073
25 changed files with 160 additions and 65 deletions

View File

@@ -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;

View File

@@ -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]) => {

View File

@@ -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),

View File

@@ -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)}

View File

@@ -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

View File

@@ -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}

View File

@@ -19,6 +19,7 @@ export const RawEditor: FC<Props> = ({
}) => (
<Fragment>
<CodeEditor
id="raw-editor"
showPrettifyButton
uniquenessKey={uniquenessKey}
defaultValue={content}

View File

@@ -101,6 +101,7 @@ export const EnvironmentEditor = forwardRef<EnvironmentEditorHandle, Props>(({
return (
<div className="environment-editor">
<CodeEditor
id="environment-editor"
ref={editorRef}
autoPrettify
enableNunjucks

View File

@@ -68,6 +68,7 @@ export const RequestHeadersEditor: FC<Props> = ({
return (
<div className="tall">
<CodeEditor
id="request-headers-editor"
onChange={handleBulkUpdate}
defaultValue={headersString}
enableNunjucks

View File

@@ -64,6 +64,7 @@ export const RequestParametersEditor: FC<Props> = ({
if (bulk) {
return (
<CodeEditor
id="request-parameters-editor"
onChange={handleBulkUpdate}
defaultValue={paramsString}
enableNunjucks

View File

@@ -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 })}

View File

@@ -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

View File

@@ -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"

View File

@@ -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() }))}
/>

View File

@@ -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()}

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -43,6 +43,7 @@ export const ResponseTimelineViewer: FC<Props> = ({ timeline, pinToBottom }) =>
return (
<CodeEditor
id="response-timeline-viewer"
ref={editorRef}
hideLineNumbers
readOnly

View File

@@ -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

View File

@@ -136,6 +136,7 @@ export const WebSocketActionBar: FC<ActionBarProps> = ({ request, environmentId,
>
<StyledUrlBar>
<OneLineEditor
id="websocket-url-bar"
ref={oneLineEditorRef}
onKeyDown={createKeybindingsHandler({
'Enter': () => handleSubmit(),

View File

@@ -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>
);

View File

@@ -185,6 +185,7 @@ const WebSocketRequestForm: FC<FormProps> = ({
}}
>
<CodeEditor
id="websocket-message-editor"
showPrettifyButton
uniquenessKey={request._id}
mode={previewMode}

View File

@@ -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}

View File

@@ -132,6 +132,7 @@ const UnitTestItemView = ({
}
>
<CodeEditor
id="unit-test-editor"
ref={editorRef}
dynamicHeight
showPrettifyButton