fix(editor): Support pasting values that start with = (#13699)

This commit is contained in:
Elias Meire 2025-03-05 16:13:27 +01:00 committed by GitHub
parent 906770a06a
commit 9e83ff51da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 82 additions and 1 deletions

View file

@ -82,7 +82,25 @@ class Worker {
terminate = vi.fn();
}
class DataTransfer {
private data: Record<string, unknown> = {};
setData = vi.fn((type: string, data) => {
this.data[type] = data;
});
getData = vi.fn((type) => {
if (type.startsWith('text')) type = 'text';
return this.data[type] ?? null;
});
}
Object.defineProperty(window, 'Worker', {
writable: true,
value: Worker,
});
Object.defineProperty(window, 'DataTransfer', {
writable: true,
value: DataTransfer,
});

View file

@ -164,6 +164,42 @@ describe('ParameterInput.vue', () => {
expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 'foo' })]);
});
test('should correctly handle paste events', async () => {
const { container, emitted } = renderComponent(ParameterInput, {
pinia: createTestingPinia(),
props: {
path: 'tag',
parameter: {
displayName: 'Tag',
name: 'tag',
type: 'string',
},
modelValue: '',
},
});
const input = container.querySelector('input') as HTMLInputElement;
expect(input).toBeInTheDocument();
await userEvent.click(input);
async function paste(text: string) {
const expression = new DataTransfer();
expression.setData('text', text);
await userEvent.clear(input);
await userEvent.paste(expression);
}
await paste('foo');
expect(emitted('update')).toContainEqual([expect.objectContaining({ value: 'foo' })]);
await paste('={{ $json.foo }}');
expect(emitted('update')).toContainEqual([
expect.objectContaining({ value: '={{ $json.foo }}' }),
]);
await paste('=flDvzj%y1nP');
expect(emitted('update')).toContainEqual([expect.objectContaining({ value: '==flDvzj%y1nP' })]);
});
test('should not reset the value of a multi-select with loadOptionsMethod on load', async () => {
mockNodeTypesState.getNodeParameterOptions = vi.fn(async () => [
{ name: 'ID', value: 'id' },

View file

@ -741,6 +741,30 @@ function onBlur() {
isFocused.value = false;
}
function onPaste(event: ClipboardEvent) {
const pastedText = event.clipboardData?.getData('text');
const input = event.target;
if (!(input instanceof HTMLInputElement)) return;
const start = input.selectionStart ?? 0;
// When a value starting with `=` is pasted that does not contain expression syntax ({{}})
// Add an extra `=` to go into expression mode and preserve the original pasted text
if (pastedText && pastedText.startsWith('=') && !pastedText.match(/{{.*?}}/g) && start === 0) {
event.preventDefault();
const end = input.selectionEnd ?? start;
const text = input.value;
const withExpressionPrefix = '=' + pastedText;
input.value = text.substring(0, start) + withExpressionPrefix + text.substring(end);
input.selectionStart = input.selectionEnd = start + withExpressionPrefix.length;
valueChanged(input.value);
}
}
function onResourceLocatorDrop(data: string) {
emit('drop', data);
}
@ -951,7 +975,9 @@ async function optionSelected(command: string) {
if (props.parameter.type === 'string') {
// Strip the '=' from the beginning
newValue = modelValueString.value ? modelValueString.value.toString().substring(1) : null;
newValue = modelValueString.value
? modelValueString.value.toString().replace(/^=+/, '')
: null;
} else if (newValue === null) {
// Invalid expressions land here
if (['number', 'boolean'].includes(props.parameter.type)) {
@ -1460,6 +1486,7 @@ onUpdated(async () => {
@keydown.stop
@focus="setFocus"
@blur="onBlur"
@paste="onPaste"
>
<template #suffix>
<N8nIcon