feat: Hackmation - automatically switch to expression mode (#13213)

Co-authored-by: Michael Kret <mishakret@gmail.com>
Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
Michael Kret 2025-02-25 13:14:38 +02:00 committed by GitHub
parent f8a7fb38cc
commit 6953b0d53a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 80 additions and 6 deletions

View file

@ -153,7 +153,7 @@ function optionSelected(action: string) {
<Assignment <Assignment
:model-value="assignment" :model-value="assignment"
:index="index" :index="index"
:path="`${path}.${index}`" :path="`${path}.assignments.${index}`"
:issues="getIssues(index)" :issues="getIssues(index)"
:class="$style.assignment" :class="$style.assignment"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"

View file

@ -186,7 +186,7 @@ const onBlur = (): void => {
:is-read-only="readOnly" :is-read-only="readOnly"
:parameter="leftParameter" :parameter="leftParameter"
:value="condition.leftValue" :value="condition.leftValue"
:path="`${path}.left`" :path="`${path}.leftValue`"
:class="[$style.input, $style.inputLeft]" :class="[$style.input, $style.inputLeft]"
data-test-id="filter-condition-left" data-test-id="filter-condition-left"
@update="onLeftValueChange" @update="onLeftValueChange"
@ -212,7 +212,7 @@ const onBlur = (): void => {
:options-position="breakpoint === 'default' ? 'top' : 'bottom'" :options-position="breakpoint === 'default' ? 'top' : 'bottom'"
:parameter="rightParameter" :parameter="rightParameter"
:value="condition.rightValue" :value="condition.rightValue"
:path="`${path}.right`" :path="`${path}.rightValue`"
:class="[$style.input, $style.inputRight]" :class="[$style.input, $style.inputRight]"
data-test-id="filter-condition-right" data-test-id="filter-condition-right"
@update="onRightValueChange" @update="onRightValueChange"

View file

@ -195,7 +195,7 @@ function getIssues(index: number): string[] {
:read-only="readOnly" :read-only="readOnly"
:can-remove="index !== 0 || state.paramValue.conditions.length > 1" :can-remove="index !== 0 || state.paramValue.conditions.length > 1"
:can-drag="index !== 0 || state.paramValue.conditions.length > 1" :can-drag="index !== 0 || state.paramValue.conditions.length > 1"
:path="`${path}.${index}`" :path="`${path}.conditions.${index}`"
:issues="getIssues(index)" :issues="getIssues(index)"
:class="$style.condition" :class="$style.condition"
@update="(value) => onConditionUpdate(index, value)" @update="(value) => onConditionUpdate(index, value)"

View file

@ -66,6 +66,7 @@ import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils'; import { createEventBus } from 'n8n-design-system/utils';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useElementSize } from '@vueuse/core'; import { useElementSize } from '@vueuse/core';
import { completeExpressionSyntax, isStringWithExpressionSyntax } from '@/utils/expressions';
type Picker = { $emit: (arg0: string, arg1: Date) => void }; type Picker = { $emit: (arg0: string, arg1: Date) => void };
@ -813,16 +814,25 @@ function valueChanged(value: NodeParameterValueType | {} | Date) {
if (remoteParameterOptionsLoading.value) { if (remoteParameterOptionsLoading.value) {
return; return;
} }
// Only update the value if it has changed
const oldValue = get(node.value, props.path); const oldValue = get(node.value, props.path);
if (oldValue !== undefined && oldValue === value) { if (oldValue !== undefined && oldValue === value) {
// Only update the value if it has changed
return; return;
} }
if (!oldValue && oldValue !== undefined && isStringWithExpressionSyntax(value)) {
// if empty old value and updated value has an expression, add '=' prefix to switch to expression mode
value = '=' + value;
}
if (props.parameter.name === 'nodeCredentialType') { if (props.parameter.name === 'nodeCredentialType') {
activeCredentialType.value = value as string; activeCredentialType.value = value as string;
} }
value = completeExpressionSyntax(value);
if (value instanceof Date) { if (value instanceof Date) {
value = value.toISOString(); value = value.toISOString();
} }

View file

@ -1,5 +1,11 @@
import { ExpressionError } from 'n8n-workflow'; import { ExpressionError } from 'n8n-workflow';
import { removeExpressionPrefix, stringifyExpressionResult, unwrapExpression } from './expressions'; import {
completeExpressionSyntax,
isStringWithExpressionSyntax,
removeExpressionPrefix,
stringifyExpressionResult,
unwrapExpression,
} from './expressions';
describe('Utils: Expressions', () => { describe('Utils: Expressions', () => {
describe('stringifyExpressionResult()', () => { describe('stringifyExpressionResult()', () => {
@ -53,4 +59,44 @@ describe('Utils: Expressions', () => {
expect(removeExpressionPrefix(input)).toBe(output); expect(removeExpressionPrefix(input)).toBe(output);
}); });
}); });
describe('completeExpressionSyntax', () => {
it('should complete expressions with "{{ " at the end', () => {
expect(completeExpressionSyntax('test {{ ')).toBe('=test {{ }}');
});
it('should complete expressions with "{{$" at the end', () => {
expect(completeExpressionSyntax('test {{$')).toBe('=test {{ $ }}');
});
it('should not modify already valid expressions', () => {
expect(completeExpressionSyntax('=valid expression')).toBe('=valid expression');
});
it('should return non-string values unchanged', () => {
expect(completeExpressionSyntax(123)).toBe(123);
expect(completeExpressionSyntax(true)).toBe(true);
expect(completeExpressionSyntax(null)).toBe(null);
});
});
describe('isStringWithExpressionSyntax', () => {
it('should return true for strings with expression syntax', () => {
expect(isStringWithExpressionSyntax('test {{ value }}')).toBe(true);
});
it('should return false for strings without expression syntax', () => {
expect(isStringWithExpressionSyntax('just a string')).toBe(false);
});
it('should return false for strings starting with "="', () => {
expect(isStringWithExpressionSyntax('=expression {{ value }}')).toBe(false);
});
it('should return false for non-string values', () => {
expect(isStringWithExpressionSyntax(123)).toBe(false);
expect(isStringWithExpressionSyntax(true)).toBe(false);
expect(isStringWithExpressionSyntax(null)).toBe(false);
});
});
}); });

View file

@ -139,3 +139,21 @@ export const stringifyExpressionResult = (
return typeof result.result === 'string' ? result.result : String(result.result); return typeof result.result === 'string' ? result.result : String(result.result);
}; };
export const completeExpressionSyntax = <T>(value: T) => {
if (typeof value === 'string' && !value.startsWith('=')) {
if (value.endsWith('{{ ')) return '=' + value + ' }}';
if (value.endsWith('{{$')) return '=' + value.slice(0, -1) + ' $ }}';
}
return value;
};
export const isStringWithExpressionSyntax = <T>(value: T): boolean => {
return (
typeof value === 'string' &&
!value.startsWith('=') &&
value.includes('{{') &&
value.includes('}}')
);
};