diff --git a/packages/frontend/editor-ui/src/__tests__/setup.ts b/packages/frontend/editor-ui/src/__tests__/setup.ts index 0e268a87d2..748814ccd0 100644 --- a/packages/frontend/editor-ui/src/__tests__/setup.ts +++ b/packages/frontend/editor-ui/src/__tests__/setup.ts @@ -82,7 +82,25 @@ class Worker { terminate = vi.fn(); } +class DataTransfer { + private data: Record = {}; + + 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, +}); diff --git a/packages/frontend/editor-ui/src/components/ParameterInput.test.ts b/packages/frontend/editor-ui/src/components/ParameterInput.test.ts index a8ae450e92..4d19250f01 100644 --- a/packages/frontend/editor-ui/src/components/ParameterInput.test.ts +++ b/packages/frontend/editor-ui/src/components/ParameterInput.test.ts @@ -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' }, diff --git a/packages/frontend/editor-ui/src/components/ParameterInput.vue b/packages/frontend/editor-ui/src/components/ParameterInput.vue index 510bb7b115..81851a96f2 100644 --- a/packages/frontend/editor-ui/src/components/ParameterInput.vue +++ b/packages/frontend/editor-ui/src/components/ParameterInput.vue @@ -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" >