mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-21 06:37:36 -04:00
feat: Context menu for Nunjucks tag[INS-4273] (#7828)
* Initial check-in for tag context menu * Remove useless codes
This commit is contained in:
@@ -2,7 +2,7 @@ import type { IpcMainEvent, IpcMainInvokeEvent, MenuItemConstructorOptions, Open
|
||||
import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, shell } from 'electron';
|
||||
|
||||
import { fnOrString } from '../../common/misc';
|
||||
import type { NunjucksParsedTagArg } from '../../templating/utils';
|
||||
import { extractNunjucksTagFromCoords, type NunjucksParsedTagArg, type NunjucksTagContextMenuAction } from '../../templating/utils';
|
||||
import { localTemplateTags } from '../../ui/components/templating/local-template-tags';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
|
||||
@@ -111,50 +111,78 @@ const getTemplateValue = (arg: NunjucksParsedTagArg) => {
|
||||
return arg.defaultValue;
|
||||
};
|
||||
export function registerElectronHandlers() {
|
||||
ipcMainOn('show-context-menu', (event, options) => {
|
||||
ipcMainOn('show-context-menu', (event, options: { key: string; nunjucksTag: ReturnType<typeof extractNunjucksTagFromCoords> }) => {
|
||||
const { key, nunjucksTag } = options;
|
||||
const sendNunjuckTagContextMsg = (type: NunjucksTagContextMenuAction) => {
|
||||
event.sender.send('context-menu-command', { key, nunjucksTag: { ...nunjucksTag, type } });
|
||||
};
|
||||
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 additionalArgs = l.templateTag.args?.slice(1);
|
||||
const hasSubmenu = actions?.options?.length;
|
||||
return {
|
||||
label: fnOrString(l.templateTag.displayName),
|
||||
...(!hasSubmenu ?
|
||||
{
|
||||
const baseTemplate: MenuItemConstructorOptions[] = nunjucksTag ?
|
||||
[
|
||||
{
|
||||
label: 'Edit',
|
||||
click: () => sendNunjuckTagContextMsg('edit'),
|
||||
},
|
||||
{
|
||||
label: 'Copy',
|
||||
click: () => {
|
||||
clipboard.writeText(nunjucksTag.template);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Cut',
|
||||
click: () => {
|
||||
clipboard.writeText(nunjucksTag.template);
|
||||
sendNunjuckTagContextMsg('delete');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
click: () => sendNunjuckTagContextMsg('delete'),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
] :
|
||||
[
|
||||
{
|
||||
role: 'cut',
|
||||
},
|
||||
{
|
||||
role: 'copy',
|
||||
},
|
||||
{
|
||||
role: 'paste',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
];
|
||||
const localTemplate: MenuItemConstructorOptions[] = localTemplateTags
|
||||
// sort alphabetically
|
||||
.sort((a, b) => fnOrString(a.templateTag.displayName).localeCompare(fnOrString(b.templateTag.displayName)))
|
||||
.map(l => {
|
||||
const actions = l.templateTag.args?.[0];
|
||||
const additionalArgs = 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, tag });
|
||||
},
|
||||
} :
|
||||
{
|
||||
submenu: actions?.options?.map(action => ({
|
||||
label: fnOrString(action.displayName),
|
||||
click: () => {
|
||||
const tag = `{% ${l.templateTag.name} ${l.templateTag.args?.map(getTemplateValue).join(', ')} %}`;
|
||||
event.sender.send('context-menu-command', { key: options.key, tag });
|
||||
const additionalTagFields = additionalArgs.length ? ', ' + additionalArgs.map(getTemplateValue).join(', ') : '';
|
||||
const tag = `{% ${l.templateTag.name} '${action.value}'${additionalTagFields} %}`;
|
||||
event.sender.send('context-menu-command', { key, tag });
|
||||
},
|
||||
} :
|
||||
{
|
||||
submenu: actions?.options?.map(action => ({
|
||||
label: fnOrString(action.displayName),
|
||||
click: () => {
|
||||
const additionalTagFields = additionalArgs.length ? ', ' + additionalArgs.map(getTemplateValue).join(', ') : '';
|
||||
const tag = `{% ${l.templateTag.name} '${action.value}'${additionalTagFields} %}`;
|
||||
event.sender.send('context-menu-command', { key: options.key, tag });
|
||||
},
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}),
|
||||
|
||||
];
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
})),
|
||||
}),
|
||||
};
|
||||
});
|
||||
const menu = Menu.buildFromTemplate([...baseTemplate, ...localTemplate]);
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
invariant(win, 'expected window');
|
||||
menu.popup({ window: win });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Sentry from '@sentry/electron/main';
|
||||
import type { MarkerRange } from 'codemirror';
|
||||
import { app, BrowserWindow, type IpcRendererEvent, shell } from 'electron';
|
||||
import fs from 'fs';
|
||||
|
||||
@@ -37,7 +38,7 @@ export interface RendererToMainBridgeAPI {
|
||||
curl: CurlBridgeAPI;
|
||||
trackSegmentEvent: (options: { event: string; properties?: Record<string, unknown> }) => void;
|
||||
trackPageView: (options: { name: string }) => void;
|
||||
showContextMenu: (options: { key: string }) => void;
|
||||
showContextMenu: (options: { key: string; nunjucksTag?: { template: string; range: MarkerRange } }) => void;
|
||||
database: {
|
||||
caCertificate: {
|
||||
create: (options: { parentId: string; path: string }) => Promise<string>;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { EditorFromTextArea, MarkerRange } from 'codemirror';
|
||||
|
||||
import type { DisplayName, PluginArgumentEnumOption, PluginTemplateTagActionContext } from './extensions';
|
||||
import objectPath from './third_party/objectPath';
|
||||
|
||||
@@ -36,6 +38,8 @@ export interface NunjucksParsedTag {
|
||||
disablePreview?: (arg0: NunjucksParsedTagArg[]) => boolean;
|
||||
}
|
||||
|
||||
export type NunjucksTagContextMenuAction = 'edit' | 'delete';
|
||||
|
||||
interface Key {
|
||||
name: string;
|
||||
value: any;
|
||||
@@ -284,3 +288,28 @@ export function extractVariableKey(text: string = '', line: number, column: numb
|
||||
const res = errorText?.match(regexVariable);
|
||||
return res?.[1] || '';
|
||||
}
|
||||
|
||||
export function extractNunjucksTagFromCoords(
|
||||
coordinates: { left: number; top: number },
|
||||
cm: React.MutableRefObject<EditorFromTextArea | null>
|
||||
): { range: MarkerRange; template: string } | void {
|
||||
if (cm && cm.current) {
|
||||
const { left, top } = coordinates;
|
||||
// get position from left and right position
|
||||
const textMarkerPos = cm.current.coordsChar({ left, top });
|
||||
// get textMarker from position
|
||||
const textMarker = cm.current?.getDoc().findMarksAt(textMarkerPos)[0];
|
||||
if (textMarker) {
|
||||
const range = textMarker.find() as MarkerRange;
|
||||
return {
|
||||
range,
|
||||
// @ts-expect-error __template shoule be property of nunjucks tag markText
|
||||
template: textMarker.__template || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface nunjucksTagContextMenuOptions extends Exclude<ReturnType<typeof extractNunjucksTagFromCoords>, void> {
|
||||
type: NunjucksTagContextMenuAction;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { DEBOUNCE_MILLIS, isMac } from '../../../common/constants';
|
||||
import * as misc from '../../../common/misc';
|
||||
import type { KeyCombination } from '../../../common/settings';
|
||||
import { getTagDefinitions } from '../../../templating/index';
|
||||
import type { NunjucksParsedTag } from '../../../templating/utils';
|
||||
import { extractNunjucksTagFromCoords, type NunjucksParsedTag, type nunjucksTagContextMenuOptions } from '../../../templating/utils';
|
||||
import { jsonPrettify } from '../../../utils/prettify/json';
|
||||
import { queryXPath } from '../../../utils/xpath/query';
|
||||
import { useGatedNunjucks } from '../../context/nunjucks/use-gated-nunjucks';
|
||||
@@ -26,6 +26,7 @@ import { Icon } from '../icon';
|
||||
import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from '../keydown-binder';
|
||||
import { FilterHelpModal } from '../modals/filter-help-modal';
|
||||
import { showModal } from '../modals/index';
|
||||
import { NunjucksModal } from '../modals/nunjucks-modal';
|
||||
import { isKeyCombinationInRegistry } from '../settings/shortcuts';
|
||||
import { normalizeIrregularWhitespace } from './normalizeIrregularWhitespace';
|
||||
const TAB_SIZE = 4;
|
||||
@@ -521,8 +522,34 @@ export const CodeEditor = memo(forwardRef<CodeEditorHandle, CodeEditorProps>(({
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.main.on('context-menu-command', (_, { key, tag }) =>
|
||||
id === key && codeMirror.current?.replaceSelection(tag));
|
||||
const unsubscribe = window.main.on('context-menu-command', (_, { key, tag, nunjucksTag }) => {
|
||||
if (id === key) {
|
||||
if (nunjucksTag) {
|
||||
const { type, template, range } = nunjucksTag as nunjucksTagContextMenuOptions;
|
||||
switch (type) {
|
||||
case 'edit':
|
||||
showModal(NunjucksModal, {
|
||||
template: template,
|
||||
onDone: (template: string | null) => {
|
||||
const { from, to } = range;
|
||||
codeMirror.current?.replaceRange(template!, from, to);
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
||||
case 'delete':
|
||||
const { from, to } = range;
|
||||
codeMirror.current?.replaceRange('', from, to);
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
};
|
||||
} else {
|
||||
codeMirror.current?.replaceSelection(tag);
|
||||
}
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
@@ -577,7 +604,18 @@ export const CodeEditor = memo(forwardRef<CodeEditorHandle, CodeEditorProps>(({
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
window.main.showContextMenu({ key: id });
|
||||
const target = event.target as HTMLElement;
|
||||
// right click on nunjucks tag
|
||||
if (target?.classList?.contains('nunjucks-tag')) {
|
||||
const { clientX, clientY } = event;
|
||||
const nunjucksTag = extractNunjucksTagFromCoords({ left: clientX, top: clientY }, codeMirror);
|
||||
if (nunjucksTag) {
|
||||
// show context menu for nunjucks tag
|
||||
window.main.showContextMenu({ key: id, nunjucksTag });
|
||||
}
|
||||
} else {
|
||||
window.main.showContextMenu({ key: id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -10,10 +10,12 @@ import { DEBOUNCE_MILLIS, isMac } from '../../../common/constants';
|
||||
import * as misc from '../../../common/misc';
|
||||
import type { KeyCombination } from '../../../common/settings';
|
||||
import { getTagDefinitions } from '../../../templating/index';
|
||||
import type { NunjucksParsedTag } from '../../../templating/utils';
|
||||
import { extractNunjucksTagFromCoords, type NunjucksParsedTag, type nunjucksTagContextMenuOptions } from '../../../templating/utils';
|
||||
import { useNunjucks } from '../../context/nunjucks/use-nunjucks';
|
||||
import { useEditorRefresh } from '../../hooks/use-editor-refresh';
|
||||
import { useRootLoaderData } from '../../routes/root';
|
||||
import { showModal } from '../modals';
|
||||
import { NunjucksModal } from '../modals/nunjucks-modal';
|
||||
import { isKeyCombinationInRegistry } from '../settings/shortcuts';
|
||||
export interface OneLineEditorProps {
|
||||
defaultValue: string;
|
||||
@@ -225,8 +227,34 @@ export const OneLineEditor = forwardRef<OneLineEditorHandle, OneLineEditorProps>
|
||||
}, [onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.main.on('context-menu-command', (_, { key, tag }) =>
|
||||
id === key && codeMirror.current?.replaceSelection(tag));
|
||||
const unsubscribe = window.main.on('context-menu-command', (_, { key, tag, nunjucksTag }) => {
|
||||
if (id === key) {
|
||||
if (nunjucksTag) {
|
||||
const { type, template, range } = nunjucksTag as nunjucksTagContextMenuOptions;
|
||||
switch (type) {
|
||||
case 'edit':
|
||||
showModal(NunjucksModal, {
|
||||
template: template,
|
||||
onDone: (template: string | null) => {
|
||||
const { from, to } = range;
|
||||
codeMirror.current?.replaceRange(template!, from, to);
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
||||
case 'delete':
|
||||
const { from, to } = range;
|
||||
codeMirror.current?.replaceRange('', from, to);
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
};
|
||||
} else {
|
||||
codeMirror.current?.replaceSelection(tag);
|
||||
}
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
@@ -255,7 +283,18 @@ export const OneLineEditor = forwardRef<OneLineEditorHandle, OneLineEditorProps>
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
window.main.showContextMenu({ key: id });
|
||||
const target = event.target as HTMLElement;
|
||||
// right click on nunjucks tag
|
||||
if (target?.classList?.contains('nunjucks-tag')) {
|
||||
const { clientX, clientY } = event;
|
||||
const nunjucksTag = extractNunjucksTagFromCoords({ left: clientX, top: clientY }, codeMirror);
|
||||
if (nunjucksTag) {
|
||||
// show context menu for nunjucks tag
|
||||
window.main.showContextMenu({ key: id, nunjucksTag });
|
||||
}
|
||||
} else {
|
||||
window.main.showContextMenu({ key: id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="editor__container input editor--single-line">
|
||||
|
||||
Reference in New Issue
Block a user