mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 13:27:31 -08:00
fix(editor): Filter component: do not clear expression when changing operator (#8635)
This commit is contained in:
parent
1a81d0ad5f
commit
66cbe54e1d
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
|
|
110
packages/editor-ui/src/components/FilterConditions/utils.ts
Normal file
110
packages/editor-ui/src/components/FilterConditions/utils.ts
Normal 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 };
|
||||
}
|
||||
};
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in a new issue