fix(editor): Fix an issue with overlapping elements in the Assignment component (#18041)

This commit is contained in:
Svetoslav Dekov
2025-08-08 09:34:02 +03:00
committed by GitHub
parent d6bc4abee2
commit c7108f4a06
4 changed files with 88 additions and 42 deletions

View File

@@ -1,11 +1,14 @@
import { computed, nextTick, ref } from 'vue';
import { createComponentRenderer } from '@/__tests__/render';
import { createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event';
import { fireEvent } from '@testing-library/vue';
import Assignment from './Assignment.vue';
import { defaultSettings } from '@/__tests__/defaults';
import { STORES } from '@n8n/stores';
import merge from 'lodash/merge';
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
import * as useResolvedExpression from '@/composables/useResolvedExpression';
const DEFAULT_SETUP = {
pinia: createTestingPinia({
@@ -69,4 +72,22 @@ describe('Assignment.vue', () => {
// Check if the parameter input hint is not displayed
expect(() => getByTestId('parameter-input-hint')).toThrow();
});
it('should shorten the expression preview hint if options are on the bottom', async () => {
vi.spyOn(useResolvedExpression, 'useResolvedExpression').mockReturnValueOnce({
resolvedExpressionString: ref('foo'),
resolvedExpression: ref(null),
isExpression: computed(() => true),
});
const { getByTestId } = renderComponent();
const previewValue = getByTestId('parameter-expression-preview-value');
expect(previewValue).not.toHaveClass('optionsPadding');
await fireEvent.mouseEnter(getByTestId('assignment-value'));
await nextTick();
expect(previewValue).toHaveClass('optionsPadding');
});
});

View File

@@ -25,6 +25,7 @@ interface Props {
const props = defineProps<Props>();
const assignment = ref<AssignmentValue>(props.modelValue);
const valueInputHovered = ref(false);
const emit = defineEmits<{
'update:model-value': [value: AssignmentValue];
@@ -113,6 +114,10 @@ const onRemove = (): void => {
const onBlur = (): void => {
emit('update:model-value', assignment.value);
};
const onValueInputHoverChange = (hovered: boolean): void => {
valueInputHovered.value = hovered;
};
</script>
<template>
@@ -186,11 +191,16 @@ const onBlur = (): void => {
data-test-id="assignment-value"
@update="onAssignmentValueChange"
@blur="onBlur"
@hover="onValueInputHoverChange"
/>
<ParameterInputHint
v-if="resolvedExpressionString"
data-test-id="parameter-expression-preview-value"
:class="$style.hint"
:class="{
[$style.hint]: true,
[$style.optionsPadding]:
breakpoint !== 'default' && !isReadOnly && valueInputHovered,
}"
:highlight="highlightHint"
:hint="hint"
single-line
@@ -248,6 +258,10 @@ const onBlur = (): void => {
left: 0;
right: 0;
}
.optionsPadding {
width: calc(100% - 140px);
}
}
.iconButton {

View File

@@ -1,4 +1,4 @@
import { renderComponent } from '@/__tests__/render';
import { nextTick } from 'vue';
import type { useNDVStore } from '@/stores/ndv.store';
import { createTestingPinia } from '@pinia/testing';
import type { useNodeTypesStore } from '@/stores/nodeTypes.store';
@@ -6,6 +6,8 @@ import type { useSettingsStore } from '@/stores/settings.store';
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
import ParameterInputFull from './ParameterInputFull.vue';
import { FROM_AI_AUTO_GENERATED_MARKER } from 'n8n-workflow';
import { fireEvent } from '@testing-library/vue';
import { createComponentRenderer } from '@/__tests__/render';
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
@@ -34,6 +36,7 @@ beforeEach(() => {
};
mockNodeTypesState = {
allNodeTypes: [],
getNodeType: vi.fn().mockReturnValue({}),
};
mockSettingsState = {
settings: {
@@ -62,6 +65,19 @@ vi.mock('@/stores/settings.store', () => {
};
});
const renderComponent = createComponentRenderer(ParameterInputFull, {
pinia: createTestingPinia(),
props: {
path: 'myParam',
value: '',
parameter: {
displayName: 'My Param',
name: 'myParam',
type: 'string',
},
},
});
describe('ParameterInputFull.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -73,18 +89,7 @@ describe('ParameterInputFull.vue', () => {
});
it('should render basic parameter', async () => {
mockNodeTypesState.getNodeType = vi.fn().mockReturnValue({});
const { getByTestId } = renderComponent(ParameterInputFull, {
pinia: createTestingPinia(),
props: {
path: 'myParam',
parameter: {
displayName: 'My Param',
name: 'myParam',
type: 'string',
},
},
});
const { getByTestId } = renderComponent();
expect(getByTestId('parameter-input')).toBeInTheDocument();
});
@@ -95,18 +100,7 @@ describe('ParameterInputFull.vue', () => {
subcategories: { AI: ['Tools'] },
},
});
const { getByTestId } = renderComponent(ParameterInputFull, {
pinia: createTestingPinia(),
props: {
path: 'myParam',
parameter: {
displayName: 'My Param',
name: 'myParam',
type: 'string',
},
value: '',
},
});
const { getByTestId } = renderComponent();
expect(getByTestId('parameter-input')).toBeInTheDocument();
expect(getByTestId('from-ai-override-button')).toBeInTheDocument();
});
@@ -118,15 +112,8 @@ describe('ParameterInputFull.vue', () => {
subcategories: { AI: ['Tools'] },
},
});
const { getByTestId } = renderComponent(ParameterInputFull, {
pinia: createTestingPinia(),
const { getByTestId } = renderComponent({
props: {
path: 'myParam',
parameter: {
displayName: 'My Param',
name: 'myParam',
type: 'string',
},
value: `={{
'and the air is free'
@@ -145,19 +132,27 @@ describe('ParameterInputFull.vue', () => {
subcategories: { AI: ['Tools'] },
},
});
const { queryByTestId, getByTestId } = renderComponent(ParameterInputFull, {
pinia: createTestingPinia(),
const { queryByTestId, getByTestId } = renderComponent({
props: {
path: 'myParam',
value: `={{ ${FROM_AI_AUTO_GENERATED_MARKER} $fromAI('myParam') }}`,
parameter: {
displayName: 'My Param',
name: 'myParam',
type: 'string',
},
},
});
expect(getByTestId('fromAI-override-field')).toBeInTheDocument();
expect(queryByTestId('override-button')).not.toBeInTheDocument();
});
it('should emit on wrapper hover', async () => {
const { getByTestId, emitted } = renderComponent();
const wrapper = getByTestId('input-label');
await fireEvent.mouseEnter(wrapper);
await nextTick();
expect(emitted().hover).toEqual([[true]]);
await fireEvent.mouseLeave(wrapper);
await nextTick();
expect(emitted().hover).toEqual([[true], [false]]);
});
});

View File

@@ -57,6 +57,7 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<{
blur: [];
update: [value: IUpdateInformation];
hover: [hovered: boolean];
}>();
const i18n = useI18n();
@@ -66,6 +67,7 @@ const eventBus = ref(createEventBus());
const focused = ref(false);
const menuExpanded = ref(false);
const forceShowExpression = ref(false);
const wrapperHovered = ref(false);
const ndvStore = useNDVStore();
const telemetry = useTelemetry();
@@ -139,6 +141,14 @@ function onMenuExpanded(expanded: boolean) {
menuExpanded.value = expanded;
}
function onWrapperMouseEnter() {
wrapperHovered.value = true;
}
function onWrapperMouseLeave() {
wrapperHovered.value = false;
}
function optionSelected(command: string) {
if (isContentOverride.value && command === 'resetValue') {
removeOverride(true);
@@ -249,6 +259,10 @@ watch(
},
);
watch(wrapperHovered, (hovered) => {
emit('hover', hovered);
});
const parameterInputWrapper = useTemplateRef('parameterInputWrapper');
const isSingleLineInput: ComputedRef<boolean> = computed(
() => parameterInputWrapper.value?.isSingleLineInput ?? false,
@@ -305,6 +319,8 @@ function removeOverride(clearField = false) {
:bold="false"
:size="label.size"
color="text-dark"
@mouseenter="onWrapperMouseEnter"
@mouseleave="onWrapperMouseLeave"
>
<template
v-if="showOverrideButton && !isSingleLineInput && optionsPosition === 'top'"