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

View file

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

View file

@ -13,6 +13,7 @@ import {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { OPERATORS_BY_ID, type FilterOperatorId } from './constants'; import { OPERATORS_BY_ID, type FilterOperatorId } from './constants';
import type { ConditionResult, FilterOperator } from './types'; import type { ConditionResult, FilterOperator } from './types';
import { DateTime } from 'luxon';
export const getFilterOperator = (key: string) => export const getFilterOperator = (key: string) =>
OPERATORS_BY_ID[key as FilterOperatorId] as FilterOperator; OPERATORS_BY_ID[key as FilterOperatorId] as FilterOperator;
@ -98,7 +99,7 @@ export const resolveCondition = ({
index, index,
errorFormat: 'inline', errorFormat: 'inline',
}); });
return { status: 'success', result }; return { status: 'success', result, resolved };
} catch (error) { } catch (error) {
let errorMessage = i18n.baseText('parameterInput.error'); let errorMessage = i18n.baseText('parameterInput.error');
@ -108,6 +109,7 @@ export const resolveCondition = ({
return { return {
status: 'validation_error', status: 'validation_error',
error: errorMessage, error: errorMessage,
resolved,
}; };
} }
} catch (error) { } catch (error) {
@ -135,3 +137,20 @@ export const operatorTypeToNodeProperty = (
return { type: operatorType }; 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';
};