mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -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 ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||||
import ParameterIssues from '@/components/ParameterIssues.vue';
|
import ParameterIssues from '@/components/ParameterIssues.vue';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { resolveParameter } from '@/composables/useWorkflowHelpers';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import {
|
import {
|
||||||
FilterError,
|
|
||||||
executeFilterCondition,
|
|
||||||
validateFieldType,
|
|
||||||
type FilterConditionValue,
|
type FilterConditionValue,
|
||||||
type FilterOperatorType,
|
|
||||||
type FilterOptionsValue,
|
type FilterOptionsValue,
|
||||||
type INodeProperties,
|
type INodeProperties,
|
||||||
type NodeParameterValue,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import OperatorSelect from './OperatorSelect.vue';
|
import OperatorSelect from './OperatorSelect.vue';
|
||||||
import { OPERATORS_BY_ID, type FilterOperatorId } from './constants';
|
import { type FilterOperatorId } from './constants';
|
||||||
import type { FilterOperator } from './types';
|
import {
|
||||||
type ConditionResult =
|
getFilterOperator,
|
||||||
| { status: 'resolve_error' }
|
handleOperatorChange,
|
||||||
| { status: 'validation_error'; error: string }
|
operatorTypeToNodeProperty,
|
||||||
| { status: 'success'; result: boolean };
|
resolveCondition,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
path: string;
|
path: string;
|
||||||
|
@ -41,6 +36,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
canRemove: true,
|
canRemove: true,
|
||||||
fixedLeftValue: false,
|
fixedLeftValue: false,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
|
index: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -56,59 +52,11 @@ const operatorId = computed<FilterOperatorId>(() => {
|
||||||
const { type, operation } = props.condition.operator;
|
const { type, operation } = props.condition.operator;
|
||||||
return `${type}:${operation}` as FilterOperatorId;
|
return `${type}:${operation}` as FilterOperatorId;
|
||||||
});
|
});
|
||||||
const operator = computed(() => OPERATORS_BY_ID[operatorId.value] as FilterOperator);
|
const operator = computed(() => getFilterOperator(operatorId.value));
|
||||||
|
|
||||||
const operatorTypeToNodeProperty = (
|
const conditionResult = computed(() =>
|
||||||
operatorType: FilterOperatorType,
|
resolveCondition({ condition: condition.value, options: props.options }),
|
||||||
): 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 allIssues = computed(() => {
|
const allIssues = computed(() => {
|
||||||
if (conditionResult.value.status === 'validation_error') {
|
if (conditionResult.value.status === 'validation_error') {
|
||||||
|
@ -150,38 +98,14 @@ const onRightValueChange = (update: IUpdateInformation): void => {
|
||||||
condition.value.rightValue = update.value;
|
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 onOperatorChange = (value: string): void => {
|
||||||
const newOperator = OPERATORS_BY_ID[value as FilterOperatorId] as FilterOperator;
|
const newOperator = getFilterOperator(value);
|
||||||
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;
|
|
||||||
|
|
||||||
// Try to convert left & right values to operator type
|
condition.value = handleOperatorChange({
|
||||||
if (leftTypeChanged) {
|
condition: condition.value,
|
||||||
condition.value.leftValue = convertToType(condition.value.leftValue, newOperator.type);
|
newOperator,
|
||||||
}
|
});
|
||||||
if (rightTypeChanged && !newOperator.singleValue) {
|
|
||||||
condition.value.rightValue = convertToType(condition.value.rightValue, newRightType);
|
|
||||||
}
|
|
||||||
|
|
||||||
condition.value.operator = {
|
|
||||||
type: newOperator.type,
|
|
||||||
operation: newOperator.operation,
|
|
||||||
rightType: newOperator.rightType,
|
|
||||||
singleValue: newOperator.singleValue,
|
|
||||||
};
|
|
||||||
emit('update', condition.value);
|
emit('update', condition.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { OPERATORS_BY_ID, OPERATOR_GROUPS } from './constants';
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { OPERATOR_GROUPS } from './constants';
|
||||||
import type { FilterOperator } from './types';
|
import type { FilterOperator } from './types';
|
||||||
|
import { getFilterOperator } from './utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selected: string;
|
selected: string;
|
||||||
|
@ -28,7 +29,7 @@ const selectedGroupIcon = computed(
|
||||||
() => groups.find((group) => group.id === selected.value.split(':')[0])?.icon,
|
() => 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 => {
|
const onOperatorChange = (operator: string): void => {
|
||||||
selected.value = operator;
|
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;
|
icon?: string;
|
||||||
children: FilterOperator[];
|
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';
|
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) => {
|
export const isTestableExpression = (expr: string) => {
|
||||||
return ExpressionParser.splitExpression(expr).every((c) => {
|
return ExpressionParser.splitExpression(expr).every((c) => {
|
||||||
|
|
Loading…
Reference in a new issue