fix(editor): Highlight matching type in filter component (#10425)

This commit is contained in:
Elias Meire 2024-08-15 15:43:25 +02:00 committed by GitHub
parent e7ee10f243
commit 6bca879d4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 70 additions and 15 deletions

View file

@ -17,6 +17,7 @@ import { type FilterOperatorId } from './constants';
import {
getFilterOperator,
handleOperatorChange,
inferOperatorType,
isEmptyInput,
operatorTypeToNodeProperty,
resolveCondition,
@ -70,6 +71,14 @@ const conditionResult = computed(() =>
resolveCondition({ condition: condition.value, options: props.options }),
);
const suggestedType = computed(() => {
if (conditionResult.value.status !== 'resolve_error') {
return inferOperatorType(conditionResult.value.resolved.leftValue);
}
return 'any';
});
const allIssues = computed(() => {
if (conditionResult.value.status === 'validation_error' && !isEmpty.value) {
return [conditionResult.value.error];
@ -176,6 +185,7 @@ const onBlur = (): void => {
<template #middle>
<OperatorSelect
:selected="`${operator.type}:${operator.operation}`"
:suggested-type="suggestedType"
:read-only="readOnly"
@operator-change="onOperatorChange"
></OperatorSelect>

View file

@ -4,13 +4,15 @@ import { computed, ref } from 'vue';
import { OPERATOR_GROUPS } from './constants';
import type { FilterOperator } from './types';
import { getFilterOperator } from './utils';
import type { FilterOperatorType } from 'n8n-workflow';
interface Props {
selected: string;
suggestedType?: FilterOperatorType;
readOnly?: boolean;
}
const props = withDefaults(defineProps<Props>(), { readOnly: false });
const props = withDefaults(defineProps<Props>(), { readOnly: false, suggestedType: 'any' });
const selected = ref(props.selected);
const menuOpen = ref(false);
@ -31,6 +33,8 @@ const selectedGroupIcon = computed(
const selectedOperator = computed(() => getFilterOperator(selected.value));
const selectedType = computed(() => selectedOperator.value.type);
const onOperatorChange = (operator: string): void => {
selected.value = operator;
emit('operatorChange', operator);
@ -65,12 +69,7 @@ function onGroupSelect(group: string) {
@mouseenter="shouldRenderItems = true"
>
<template v-if="selectedGroupIcon" #prefix>
<n8n-icon
:class="$style.selectedGroupIcon"
:icon="selectedGroupIcon"
color="text-light"
size="small"
/>
<n8n-icon :class="$style.icon" :icon="selectedGroupIcon" color="text-light" size="small" />
</template>
<div v-if="shouldRenderItems" :class="$style.groups">
<div v-for="group of groups" :key="group.name">
@ -84,12 +83,18 @@ function onGroupSelect(group: string) {
>
<template #reference>
<div
:class="$style.group"
:class="[
$style.group,
{
[$style.selected]: group.id === selectedType,
[$style.suggested]: group.id === suggestedType,
},
]"
@mouseenter="() => onGroupSelect(group.id)"
@click="() => onGroupSelect(group.id)"
>
<div :class="$style.groupTitle">
<n8n-icon v-if="group.icon" :icon="group.icon" color="text-light" size="small" />
<n8n-icon v-if="group.icon" :icon="group.icon" :class="$style.icon" size="small" />
<span>{{ i18n.baseText(group.name) }}</span>
</div>
<n8n-icon icon="chevron-right" color="text-light" size="xsmall" />
@ -116,7 +121,7 @@ function onGroupSelect(group: string) {
</template>
<style lang="scss" module>
.selectedGroupIcon {
.icon {
color: var(--color-text-light);
}
@ -137,6 +142,10 @@ function onGroupSelect(group: string) {
padding: var(--spacing-2xs) var(--spacing-s);
cursor: pointer;
&.suggested {
font-weight: 800;
}
&:hover {
background: var(--color-background-base);
}

View file

@ -1,4 +1,4 @@
import { getFilterOperator, handleOperatorChange } from '../utils';
import { getFilterOperator, handleOperatorChange, inferOperatorType } from '../utils';
describe('FilterConditions > utils', () => {
describe('handleOperatorChange', () => {
@ -71,4 +71,17 @@ describe('FilterConditions > utils', () => {
});
});
});
describe('inferOperatorType', () => {
test.each([
['hello world', 'string'],
[14.2, 'number'],
[false, 'boolean'],
[[{ a: 1 }], 'array'],
[{ a: 1 }, 'object'],
['2024-01-01', 'dateTime'],
])('should correctly infer %s type', (value, result) => {
expect(inferOperatorType(value)).toBe(result);
});
});
});

View file

@ -1,5 +1,5 @@
import type { BaseTextKey } from '@/plugins/i18n';
import type { FilterOperatorValue } from 'n8n-workflow';
import type { FilterConditionValue, FilterOperatorValue } from 'n8n-workflow';
export interface FilterOperator extends FilterOperatorValue {
name: BaseTextKey;
@ -14,5 +14,9 @@ export interface FilterOperatorGroup {
export type ConditionResult =
| { status: 'resolve_error' }
| { status: 'validation_error'; error: string }
| { status: 'success'; result: boolean };
| { status: 'validation_error'; error: string; resolved: FilterConditionValue }
| {
status: 'success';
result: boolean;
resolved: FilterConditionValue;
};

View file

@ -13,6 +13,7 @@ import {
} from 'n8n-workflow';
import { OPERATORS_BY_ID, type FilterOperatorId } from './constants';
import type { ConditionResult, FilterOperator } from './types';
import { DateTime } from 'luxon';
export const getFilterOperator = (key: string) =>
OPERATORS_BY_ID[key as FilterOperatorId] as FilterOperator;
@ -98,7 +99,7 @@ export const resolveCondition = ({
index,
errorFormat: 'inline',
});
return { status: 'success', result };
return { status: 'success', result, resolved };
} catch (error) {
let errorMessage = i18n.baseText('parameterInput.error');
@ -108,6 +109,7 @@ export const resolveCondition = ({
return {
status: 'validation_error',
error: errorMessage,
resolved,
};
}
} catch (error) {
@ -135,3 +137,20 @@ export const operatorTypeToNodeProperty = (
return { type: operatorType };
}
};
export const inferOperatorType = (value: unknown): FilterOperatorType => {
if (typeof value === 'string') {
if (validateFieldType('filter', value, 'dateTime').valid) return 'dateTime';
return 'string';
} else if (typeof value === 'number') {
return 'number';
} else if (typeof value === 'boolean') {
return 'boolean';
} else if (DateTime.isDateTime(value)) {
return 'dateTime';
} else if (value && typeof value === 'object') {
return Array.isArray(value) ? 'array' : 'object';
}
return 'any';
};