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
:model-value="assignment"
:index="index"
:path="`${path}.${index}`"
:path="`${path}.assignments.${index}`"
:issues="getIssues(index)"
:class="$style.assignment"
:is-read-only="isReadOnly"

View file

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

View file

@ -195,7 +195,7 @@ function getIssues(index: number): string[] {
:read-only="readOnly"
:can-remove="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)"
:class="$style.condition"
@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 { useRouter } from 'vue-router';
import { useElementSize } from '@vueuse/core';
import { completeExpressionSyntax, isStringWithExpressionSyntax } from '@/utils/expressions';
type Picker = { $emit: (arg0: string, arg1: Date) => void };
@ -813,16 +814,25 @@ function valueChanged(value: NodeParameterValueType | {} | Date) {
if (remoteParameterOptionsLoading.value) {
return;
}
// Only update the value if it has changed
const oldValue = get(node.value, props.path);
if (oldValue !== undefined && oldValue === value) {
// Only update the value if it has changed
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') {
activeCredentialType.value = value as string;
}
value = completeExpressionSyntax(value);
if (value instanceof Date) {
value = value.toISOString();
}

View file

@ -1,5 +1,11 @@
import { ExpressionError } from 'n8n-workflow';
import { removeExpressionPrefix, stringifyExpressionResult, unwrapExpression } from './expressions';
import {
completeExpressionSyntax,
isStringWithExpressionSyntax,
removeExpressionPrefix,
stringifyExpressionResult,
unwrapExpression,
} from './expressions';
describe('Utils: Expressions', () => {
describe('stringifyExpressionResult()', () => {
@ -53,4 +59,44 @@ describe('Utils: Expressions', () => {
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);
};
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('}}')
);
};