fix(editor): Filter component: do not clear expression when changing operator (#8635)

This commit is contained in:
Elias Meire 2024-02-16 14:32:43 +01:00 committed by GitHub
parent 1a81d0ad5f
commit 66cbe54e1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 207 additions and 96 deletions

View file

@ -4,26 +4,21 @@ import InputTriple from '@/components/InputTriple/InputTriple.vue';
import ParameterInputFull from '@/components/ParameterInputFull.vue';
import ParameterIssues from '@/components/ParameterIssues.vue';
import { useI18n } from '@/composables/useI18n';
import { resolveParameter } from '@/composables/useWorkflowHelpers';
import { DateTime } from 'luxon';
import {
FilterError,
executeFilterCondition,
validateFieldType,
type FilterConditionValue,
type FilterOperatorType,
type FilterOptionsValue,
type INodeProperties,
type NodeParameterValue,
} from 'n8n-workflow';
import { computed, ref } from 'vue';
import OperatorSelect from './OperatorSelect.vue';
import { OPERATORS_BY_ID, type FilterOperatorId } from './constants';
import type { FilterOperator } from './types';
type ConditionResult =
| { status: 'resolve_error' }
| { status: 'validation_error'; error: string }
| { status: 'success'; result: boolean };
import { type FilterOperatorId } from './constants';
import {
getFilterOperator,
handleOperatorChange,
operatorTypeToNodeProperty,
resolveCondition,
} from './utils';
interface Props {
path: string;
@ -41,6 +36,7 @@ const props = withDefaults(defineProps<Props>(), {
canRemove: true,
fixedLeftValue: false,
readOnly: false,
index: 0,
});
const emit = defineEmits<{
@ -56,59 +52,11 @@ const operatorId = computed<FilterOperatorId>(() => {
const { type, operation } = props.condition.operator;
return `${type}:${operation}` as FilterOperatorId;
});
const operator = computed(() => OPERATORS_BY_ID[operatorId.value] as FilterOperator);
const operator = computed(() => getFilterOperator(operatorId.value));
const operatorTypeToNodeProperty = (
operatorType: FilterOperatorType,
): Pick<INodeProperties, 'type' | 'options'> => {
switch (operatorType) {
case 'boolean':
return {
type: 'options',
options: [
{ name: 'true', value: true },
{ name: 'false', value: false },
],
};
case 'array':
case 'object':
case 'any':
return { type: 'string' };
default:
return { type: operatorType };
}
};
const conditionResult = computed<ConditionResult>(() => {
try {
const resolved = resolveParameter(
condition.value as unknown as NodeParameterValue,
) as FilterConditionValue;
if (resolved.leftValue === undefined || resolved.rightValue === undefined) {
return { status: 'resolve_error' };
}
try {
const result = executeFilterCondition(resolved, props.options, {
index: props.index ?? 0,
errorFormat: 'inline',
});
return { status: 'success', result };
} catch (error) {
let errorMessage = i18n.baseText('parameterInput.error');
if (error instanceof FilterError) {
errorMessage = `${error.message}.\n${error.description}`;
}
return {
status: 'validation_error',
error: errorMessage,
};
}
} catch (error) {
return { status: 'resolve_error' };
}
});
const conditionResult = computed(() =>
resolveCondition({ condition: condition.value, options: props.options }),
);
const allIssues = computed(() => {
if (conditionResult.value.status === 'validation_error') {
@ -150,38 +98,14 @@ const onRightValueChange = (update: IUpdateInformation): void => {
condition.value.rightValue = update.value;
};
const convertToType = (value: unknown, type: FilterOperatorType): unknown => {
if (type === 'any') return value;
const fallback = type === 'boolean' ? false : value;
return (
validateFieldType('filter', condition.value.leftValue, type, { parseStrings: true }).newValue ??
fallback
);
};
const onOperatorChange = (value: string): void => {
const newOperator = OPERATORS_BY_ID[value as FilterOperatorId] as FilterOperator;
const rightType = operator.value.rightType ?? operator.value.type;
const newRightType = newOperator.rightType ?? newOperator.type;
const leftTypeChanged = operator.value.type !== newOperator.type;
const rightTypeChanged = rightType !== newRightType;
const newOperator = getFilterOperator(value);
// Try to convert left & right values to operator type
if (leftTypeChanged) {
condition.value.leftValue = convertToType(condition.value.leftValue, newOperator.type);
}
if (rightTypeChanged && !newOperator.singleValue) {
condition.value.rightValue = convertToType(condition.value.rightValue, newRightType);
}
condition.value = handleOperatorChange({
condition: condition.value,
newOperator,
});
condition.value.operator = {
type: newOperator.type,
operation: newOperator.operation,
rightType: newOperator.rightType,
singleValue: newOperator.singleValue,
};
emit('update', condition.value);
};

View file

@ -1,8 +1,9 @@
<script setup lang="ts">
import { useI18n } from '@/composables/useI18n';
import { OPERATORS_BY_ID, OPERATOR_GROUPS } from './constants';
import { computed, ref } from 'vue';
import { OPERATOR_GROUPS } from './constants';
import type { FilterOperator } from './types';
import { getFilterOperator } from './utils';
interface Props {
selected: string;
@ -28,7 +29,7 @@ const selectedGroupIcon = computed(
() => groups.find((group) => group.id === selected.value.split(':')[0])?.icon,
);
const selectedOperator = computed(() => OPERATORS_BY_ID[selected.value] as FilterOperator);
const selectedOperator = computed(() => getFilterOperator(selected.value));
const onOperatorChange = (operator: string): void => {
selected.value = operator;

View file

@ -0,0 +1,68 @@
import { getFilterOperator, handleOperatorChange } from '../utils';
describe('FilterConditions > utils', () => {
describe('handleOperatorChange', () => {
test('should convert string > number', () => {
const condition = {
id: '1',
leftValue: '45',
rightValue: 'notANumber',
operator: getFilterOperator('string:equals'),
};
expect(
handleOperatorChange({
condition,
newOperator: getFilterOperator('number:equals'),
}),
).toEqual({
id: '1',
leftValue: 45,
rightValue: 'notANumber',
operator: {
name: 'filter.operator.equals',
operation: 'equals',
type: 'string',
},
});
});
test('should convert string > boolean', () => {
const condition = {
id: '1',
leftValue: 'notABool',
rightValue: 'true',
operator: getFilterOperator('string:equals'),
};
expect(
handleOperatorChange({
condition,
newOperator: getFilterOperator('boolean:equals'),
}),
).toEqual({
id: '1',
leftValue: false,
rightValue: true,
operator: {
name: 'filter.operator.equals',
operation: 'equals',
type: 'string',
},
});
});
test('should not convert or clear expressions', () => {
const condition = {
id: '1',
leftValue: '={{ $json.foo }}',
rightValue: '={{ $("nodename").foo }}',
operator: getFilterOperator('string:equals'),
};
expect(
handleOperatorChange({
condition,
newOperator: getFilterOperator('boolean:equals'),
}),
).toEqual(condition);
});
});
});

View file

@ -11,3 +11,8 @@ export interface FilterOperatorGroup {
icon?: string;
children: FilterOperator[];
}
export type ConditionResult =
| { status: 'resolve_error' }
| { status: 'validation_error'; error: string }
| { status: 'success'; result: boolean };

View file

@ -0,0 +1,110 @@
import { resolveParameter } from '@/composables/useWorkflowHelpers';
import { i18n } from '@/plugins/i18n';
import { isExpression } from '@/utils/expressions';
import {
FilterError,
executeFilterCondition,
validateFieldType,
type FilterConditionValue,
type FilterOperatorType,
type FilterOptionsValue,
type NodeParameterValue,
type INodeProperties,
} from 'n8n-workflow';
import { OPERATORS_BY_ID, type FilterOperatorId } from './constants';
import type { ConditionResult, FilterOperator } from './types';
export const getFilterOperator = (key: string) =>
OPERATORS_BY_ID[key as FilterOperatorId] as FilterOperator;
const convertToType = (value: unknown, type: FilterOperatorType): unknown => {
if (type === 'any') return value;
const fallback = type === 'boolean' ? false : value;
return validateFieldType('filter', value, type, { parseStrings: true }).newValue ?? fallback;
};
export const handleOperatorChange = ({
condition,
newOperator,
}: {
condition: FilterConditionValue;
newOperator: FilterOperator;
}): FilterConditionValue => {
const currentOperator = condition.operator;
const rightType = currentOperator.rightType ?? currentOperator.type;
const newRightType = newOperator.rightType ?? newOperator.type;
const leftTypeChanged = currentOperator.type !== newOperator.type;
const rightTypeChanged = rightType !== newRightType;
// Try to convert left & right values to operator type
if (leftTypeChanged && !isExpression(condition.leftValue)) {
condition.leftValue = convertToType(condition.leftValue, newOperator.type);
}
if (rightTypeChanged && !newOperator.singleValue && !isExpression(condition.rightValue)) {
condition.rightValue = convertToType(condition.rightValue, newRightType);
}
return condition;
};
export const resolveCondition = ({
condition,
options,
index = 0,
}: {
condition: FilterConditionValue;
options: FilterOptionsValue;
index?: number;
}): ConditionResult => {
try {
const resolved = resolveParameter(
condition as unknown as NodeParameterValue,
) as FilterConditionValue;
if (resolved.leftValue === undefined || resolved.rightValue === undefined) {
return { status: 'resolve_error' };
}
try {
const result = executeFilterCondition(resolved, options, {
index,
errorFormat: 'inline',
});
return { status: 'success', result };
} catch (error) {
let errorMessage = i18n.baseText('parameterInput.error');
if (error instanceof FilterError) {
errorMessage = `${error.message}.\n${error.description}`;
}
return {
status: 'validation_error',
error: errorMessage,
};
}
} catch (error) {
return { status: 'resolve_error' };
}
};
export const operatorTypeToNodeProperty = (
operatorType: FilterOperatorType,
): Pick<INodeProperties, 'type' | 'options'> => {
switch (operatorType) {
case 'boolean':
return {
type: 'options',
options: [
{ name: 'true', value: true },
{ name: 'false', value: false },
],
};
case 'array':
case 'object':
case 'any':
return { type: 'string' };
default:
return { type: operatorType };
}
};

View file

@ -1,6 +1,9 @@
import { ExpressionParser } from 'n8n-workflow';
export const isExpression = (expr: string) => expr.startsWith('=');
export const isExpression = (expr: unknown) => {
if (typeof expr !== 'string') return false;
return expr.startsWith('=');
};
export const isTestableExpression = (expr: string) => {
return ExpressionParser.splitExpression(expr).every((c) => {