feat(editor): Update element-plus to 2.4.3 (no-changelog) (#10281)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Milorad FIlipović 2024-08-21 10:42:08 +02:00 committed by GitHub
parent 03c19723d2
commit ecd287564d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 331 additions and 293 deletions

View file

@ -11,6 +11,21 @@ describe('Inline expression editor', () => {
cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError'); cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
}); });
describe('Basic UI functionality', () => {
it('should open and close inline expression preview', () => {
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.openNode('Schedule');
WorkflowPage.actions.openInlineExpressionEditor();
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('123');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^123$/);
// click outside to close
ndv.getters.outputPanel().click();
WorkflowPage.getters.inlineExpressionEditorOutput().should('not.exist');
});
});
describe('Static data', () => { describe('Static data', () => {
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.addNodeToCanvas('Hacker News'); WorkflowPage.actions.addNodeToCanvas('Hacker News');

View file

@ -65,7 +65,7 @@ describe('Workflow tags', () => {
it('should detach a tag inline by clicking on dropdown list item', () => { it('should detach a tag inline by clicking on dropdown list item', () => {
wf.getters.createTagButton().click(); wf.getters.createTagButton().click();
wf.actions.addTags(TEST_TAGS); wf.actions.addTags(TEST_TAGS);
wf.getters.nthTagPill(1).click(); wf.getters.workflowTagsContainer().click();
wf.getters.tagsInDropdown().filter('.selected').first().click(); wf.getters.tagsInDropdown().filter('.selected').first().click();
cy.get('body').click(0, 0); cy.get('body').click(0, 0);
wf.getters.workflowTags().click(); wf.getters.workflowTags().click();
@ -79,7 +79,7 @@ describe('Workflow tags', () => {
wf.actions.addTags(TEST_TAGS); wf.actions.addTags(TEST_TAGS);
cy.get('body').click(0, 0); cy.get('body').click(0, 0);
wf.getters.workflowTags().click(); wf.getters.workflowTags().click();
wf.getters.tagsDropdown().find('input:focus').type(NON_EXISTING_TAG); wf.getters.workflowTagsInput().type(NON_EXISTING_TAG);
getVisibleSelect() getVisibleSelect()
.find('li') .find('li')

View file

@ -112,13 +112,13 @@ describe('Credentials', () => {
workflowPage.getters.nodeCredentialsSelect().should('have.length', 2); workflowPage.getters.nodeCredentialsSelect().should('have.length', 2);
workflowPage.getters.nodeCredentialsSelect().first().click(); workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect().find('li').last().click(); getVisibleSelect().find('li').contains('Create New Credential').click();
// This one should show auth type selector // This one should show auth type selector
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
cy.get('body').type('{esc}'); cy.get('body').type('{esc}');
workflowPage.getters.nodeCredentialsSelect().last().click(); workflowPage.getters.nodeCredentialsSelect().last().click();
getVisibleSelect().find('li').last().click(); getVisibleSelect().find('li').contains('Create New Credential').click();
// This one should not show auth type selector // This one should not show auth type selector
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist'); credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
}); });

View file

@ -138,6 +138,8 @@ export class NDV extends BasePage {
cy.getByTestId(`fixed-collection-${paramName}`), cy.getByTestId(`fixed-collection-${paramName}`),
schemaViewNode: () => cy.getByTestId('run-data-schema-node'), schemaViewNode: () => cy.getByTestId('run-data-schema-node'),
schemaViewNodeName: () => cy.getByTestId('run-data-schema-node-name'), schemaViewNodeName: () => cy.getByTestId('run-data-schema-node-name'),
expressionExpanders: () => cy.getByTestId('expander'),
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
}; };
actions = { actions = {

View file

@ -3,7 +3,7 @@ export function getPopper() {
} }
export function getVisiblePopper() { export function getVisiblePopper() {
return getPopper().filter(':visible'); return getPopper().filter('[aria-hidden="false"]');
} }
export function getVisibleSelect() { export function getVisibleSelect() {

View file

@ -8,7 +8,7 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import { fas } from '@fortawesome/free-solid-svg-icons'; import { fas } from '@fortawesome/free-solid-svg-icons';
import ElementPlus from 'element-plus'; import ElementPlus from 'element-plus';
import lang from 'element-plus/lib/locale/lang/en'; import lang from 'element-plus/dist/locale/en.mjs'
import { N8nPlugin } from '../src/plugin'; import { N8nPlugin } from '../src/plugin';

View file

@ -44,7 +44,7 @@
"@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.3", "@fortawesome/vue-fontawesome": "^3.0.3",
"element-plus": "^2.3.6", "element-plus": "2.4.3",
"markdown-it": "^13.0.2", "markdown-it": "^13.0.2",
"markdown-it-emoji": "^2.0.2", "markdown-it-emoji": "^2.0.2",
"markdown-it-link-attributes": "^4.0.1", "markdown-it-link-attributes": "^4.0.1",

View file

@ -10,7 +10,6 @@ exports[`components > N8nCheckbox > should render with both child and label 1`]
class="el-checkbox__input" class="el-checkbox__input"
> >
<input <input
aria-hidden="false"
class="el-checkbox__original" class="el-checkbox__original"
type="checkbox" type="checkbox"
value="Checkbox" value="Checkbox"
@ -71,7 +70,6 @@ exports[`components > N8nCheckbox > should render with child 1`] = `
class="el-checkbox__input" class="el-checkbox__input"
> >
<input <input
aria-hidden="false"
class="el-checkbox__original" class="el-checkbox__original"
type="checkbox" type="checkbox"
/> />
@ -106,7 +104,6 @@ exports[`components > N8nCheckbox > should render with label 1`] = `
class="el-checkbox__input" class="el-checkbox__input"
> >
<input <input
aria-hidden="false"
class="el-checkbox__original" class="el-checkbox__original"
type="checkbox" type="checkbox"
value="Checkbox" value="Checkbox"
@ -164,7 +161,6 @@ exports[`components > N8nCheckbox > should render without label and child conten
class="el-checkbox__input" class="el-checkbox__input"
> >
<input <input
aria-hidden="false"
class="el-checkbox__original" class="el-checkbox__original"
type="checkbox" type="checkbox"
/> />

View file

@ -9,6 +9,7 @@ exports[`components > N8nColorPicker > should render with input 1`] = `
<div <div
aria-description="current color is . press enter to select a new color." aria-description="current color is . press enter to select a new color."
aria-disabled="false"
aria-label="color picker" aria-label="color picker"
class="el-color-picker el-color-picker--large el-tooltip__trigger el-tooltip__trigger" class="el-color-picker el-color-picker--large el-tooltip__trigger el-tooltip__trigger"
role="button" role="button"
@ -106,6 +107,7 @@ exports[`components > N8nColorPicker > should render without input 1`] = `
<div <div
aria-description="current color is . press enter to select a new color." aria-description="current color is . press enter to select a new color."
aria-disabled="false"
aria-label="color picker" aria-label="color picker"
class="el-color-picker el-color-picker--large el-tooltip__trigger el-tooltip__trigger" class="el-color-picker el-color-picker--large el-tooltip__trigger el-tooltip__trigger"
role="button" role="button"

View file

@ -1,6 +1,7 @@
import { render } from '@testing-library/vue'; import { render } from '@testing-library/vue';
import N8nDatatable from '../Datatable.vue'; import N8nDatatable from '../Datatable.vue';
import { rows, columns } from './data'; import { rows, columns } from './data';
import { removeDynamicAttributes } from 'n8n-design-system/utils';
const stubs = [ const stubs = [
'n8n-option', 'n8n-option',
@ -33,6 +34,7 @@ describe('components', () => {
expect(wrapper.container.querySelectorAll('tbody tr td').length).toEqual( expect(wrapper.container.querySelectorAll('tbody tr td').length).toEqual(
columns.length * rowsPerPage, columns.length * rowsPerPage,
); );
removeDynamicAttributes(wrapper.container);
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });

View file

@ -113,7 +113,6 @@ exports[`components > N8nDatatable > should render correctly 1`] = `
<div class="el-select el-select--small" popperappendtobody="false" limitpopperwidth="false"> <div class="el-select el-select--small" popperappendtobody="false" limitpopperwidth="false">
<div class="select-trigger el-tooltip__trigger el-tooltip__trigger"> <div class="select-trigger el-tooltip__trigger el-tooltip__trigger">
<!--v-if--> <!--v-if-->
<!-- fix: https://github.com/element-plus/element-plus/issues/11415 -->
<!--v-if--> <!--v-if-->
<div class="el-input el-input--small el-input--suffix"> <div class="el-input el-input--small el-input--suffix">
<!-- input --> <!-- input -->
@ -121,7 +120,7 @@ exports[`components > N8nDatatable > should render correctly 1`] = `
<!--v-if--> <!--v-if-->
<div class="el-input__wrapper"> <div class="el-input__wrapper">
<!-- prefix slot --> <!-- prefix slot -->
<!--v-if--><input class="el-input__inner" type="text" readonly="" autocomplete="off" tabindex="0" placeholder="Select"><!-- suffix slot --><span class="el-input__suffix"><span class="el-input__suffix-inner"><i class="el-icon el-select__caret el-select__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"></path></svg></i><!--v-if--><!--v-if--><!--v-if--><!--v-if--><!--v-if--><!--v-if--></span></span> <!--v-if--><input class="el-input__inner" role="combobox" aria-activedescendant="" aria-expanded="false" aria-autocomplete="none" aria-haspopup="listbox" type="text" readonly="" autocomplete="off" tabindex="0" placeholder="Select"><!-- suffix slot --><span class="el-input__suffix"><span class="el-input__suffix-inner"><i class="el-icon el-select__caret el-select__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"></path></svg></i><!--v-if--><!--v-if--><!--v-if--><!--v-if--><!--v-if--><!--v-if--></span></span>
</div><!-- append slot --> </div><!-- append slot -->
<!--v-if--> <!--v-if-->
</div> </div>

View file

@ -3,6 +3,7 @@ import { render, waitFor, within } from '@testing-library/vue';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import N8nSelect from '../Select.vue'; import N8nSelect from '../Select.vue';
import N8nOption from '../../N8nOption/Option.vue'; import N8nOption from '../../N8nOption/Option.vue';
import { removeDynamicAttributes } from 'n8n-design-system/utils';
describe('components', () => { describe('components', () => {
describe('N8nSelect', () => { describe('N8nSelect', () => {
@ -21,6 +22,7 @@ describe('components', () => {
], ],
}, },
}); });
removeDynamicAttributes(wrapper.container);
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });

View file

@ -6,7 +6,6 @@ exports[`components > N8nSelect > should render correctly 1`] = `
<div class="el-select el-select--large" popperappendtobody="false" limitpopperwidth="false"> <div class="el-select el-select--large" popperappendtobody="false" limitpopperwidth="false">
<div class="select-trigger el-tooltip__trigger el-tooltip__trigger"> <div class="select-trigger el-tooltip__trigger el-tooltip__trigger">
<!--v-if--> <!--v-if-->
<!-- fix: https://github.com/element-plus/element-plus/issues/11415 -->
<!--v-if--> <!--v-if-->
<div class="el-input el-input--large el-input--suffix"> <div class="el-input el-input--large el-input--suffix">
<!-- input --> <!-- input -->
@ -14,7 +13,7 @@ exports[`components > N8nSelect > should render correctly 1`] = `
<!--v-if--> <!--v-if-->
<div class="el-input__wrapper"> <div class="el-input__wrapper">
<!-- prefix slot --> <!-- prefix slot -->
<!--v-if--><input class="el-input__inner" type="text" readonly="" autocomplete="off" tabindex="0" placeholder="Select"><!-- suffix slot --><span class="el-input__suffix"><span class="el-input__suffix-inner"><i class="el-icon el-select__caret el-select__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"></path></svg></i><!--v-if--><!--v-if--><!--v-if--><!--v-if--><!--v-if--><!--v-if--></span></span> <!--v-if--><input class="el-input__inner" role="combobox" aria-activedescendant="" aria-expanded="false" aria-autocomplete="none" aria-haspopup="listbox" type="text" readonly="" autocomplete="off" tabindex="0" placeholder="Select"><!-- suffix slot --><span class="el-input__suffix"><span class="el-input__suffix-inner"><i class="el-icon el-select__caret el-select__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"></path></svg></i><!--v-if--><!--v-if--><!--v-if--><!--v-if--><!--v-if--><!--v-if--></span></span>
</div><!-- append slot --> </div><!-- append slot -->
<!--v-if--> <!--v-if-->
</div> </div>

View file

@ -4,3 +4,4 @@ export * from './markdown';
export * from './typeguards'; export * from './typeguards';
export * from './uid'; export * from './uid';
export * from './valueByPath'; export * from './valueByPath';
export * from './testUtils';

View file

@ -0,0 +1,18 @@
const DYNAMIC_ATTRIBUTES = ['aria-controls'];
/**
* Deletes dynamic attributes from the container children so snapshots can be tested.
*
* Background:
* Vue test utils use server rendering to render components (https://v1.test-utils.vuejs.org/api/render.html#render).
* Element UI in SSR mode adds dynamic attributes to the rendered HTML each time the test is run (https://element-plus.org/en-US/guide/ssr#provide-an-id).
*
* NOTE: Make sure to manually remove same attributes from the expected snapshot.
*/
// TODO: Find a way to inject static value for dynamic attributes in tests
export function removeDynamicAttributes(container: Element): void {
DYNAMIC_ATTRIBUTES.forEach((attribute) => {
const elements = container.querySelectorAll(`[${attribute}]`);
elements.forEach((element) => element.removeAttribute(attribute));
});
}

View file

@ -72,7 +72,7 @@ export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
}; };
export const getDropdownItems = async (dropdownTriggerParent: HTMLElement) => { export const getDropdownItems = async (dropdownTriggerParent: HTMLElement) => {
await userEvent.click(within(dropdownTriggerParent).getByRole('textbox')); await userEvent.click(within(dropdownTriggerParent).getByRole('combobox'));
const selectTrigger = dropdownTriggerParent.querySelector( const selectTrigger = dropdownTriggerParent.querySelector(
'.select-trigger[aria-describedby]', '.select-trigger[aria-describedby]',
) as HTMLElement; ) as HTMLElement;
@ -84,3 +84,9 @@ export const getDropdownItems = async (dropdownTriggerParent: HTMLElement) => {
return selectDropdown.querySelectorAll('.el-select-dropdown__item'); return selectDropdown.querySelectorAll('.el-select-dropdown__item');
}; };
export const getSelectedDropdownValue = async (items: NodeListOf<Element>) => {
const selectedItem = Array.from(items).find((item) => item.classList.contains('selected'));
expect(selectedItem).toBeInTheDocument();
return selectedItem?.querySelector('p')?.textContent?.trim();
};

View file

@ -2,11 +2,13 @@
import { type ContextMenuAction, useContextMenu } from '@/composables/useContextMenu'; import { type ContextMenuAction, useContextMenu } from '@/composables/useContextMenu';
import { N8nActionDropdown } from 'n8n-design-system'; import { N8nActionDropdown } from 'n8n-design-system';
import { watch, ref } from 'vue'; import { watch, ref } from 'vue';
import { onClickOutside } from '@vueuse/core';
const contextMenu = useContextMenu(); const contextMenu = useContextMenu();
const { position, isOpen, actions, target } = contextMenu; const { position, isOpen, actions, target } = contextMenu;
const dropdown = ref<InstanceType<typeof N8nActionDropdown>>(); const dropdown = ref<InstanceType<typeof N8nActionDropdown>>();
const emit = defineEmits<{ action: [action: ContextMenuAction, nodeIds: string[]] }>(); const emit = defineEmits<{ action: [action: ContextMenuAction, nodeIds: string[]] }>();
const container = ref<HTMLDivElement>();
watch( watch(
isOpen, isOpen,
@ -26,7 +28,7 @@ function onActionSelect(item: string) {
emit('action', action, contextMenu.targetNodeIds.value); emit('action', action, contextMenu.targetNodeIds.value);
} }
function onClickOutside(event: MouseEvent) { function closeMenu(event: MouseEvent) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
contextMenu.close(); contextMenu.close();
@ -37,12 +39,14 @@ function onVisibleChange(open: boolean) {
contextMenu.close(); contextMenu.close();
} }
} }
onClickOutside(container, closeMenu);
</script> </script>
<template> <template>
<Teleport v-if="isOpen" to="body"> <Teleport v-if="isOpen" to="body">
<div <div
v-on-click-outside="onClickOutside" ref="container"
:class="$style.contextMenu" :class="$style.contextMenu"
:style="{ :style="{
left: `${position[0]}px`, left: `${position[0]}px`,

View file

@ -2,7 +2,6 @@
<ExpandableInputBase :model-value="modelValue" :placeholder="placeholder"> <ExpandableInputBase :model-value="modelValue" :placeholder="placeholder">
<input <input
ref="inputRef" ref="inputRef"
v-on-click-outside="onClickOutside"
class="el-input__inner" class="el-input__inner"
:value="modelValue" :value="modelValue"
:placeholder="placeholder" :placeholder="placeholder"
@ -19,6 +18,7 @@
import type { EventBus } from 'n8n-design-system'; import type { EventBus } from 'n8n-design-system';
import { onBeforeUnmount, onMounted, ref } from 'vue'; import { onBeforeUnmount, onMounted, ref } from 'vue';
import ExpandableInputBase from './ExpandableInputBase.vue'; import ExpandableInputBase from './ExpandableInputBase.vue';
import { onClickOutside } from '@vueuse/core';
type Props = { type Props = {
modelValue: string; modelValue: string;
@ -68,11 +68,11 @@ function onEnter() {
} }
} }
function onClickOutside(e: Event) { onClickOutside(inputRef, () => {
if (e.type === 'click' && inputRef.value) { if (inputRef.value) {
emit('blur', inputRef.value.value); emit('blur', inputRef.value.value);
} }
} });
function onEscape() { function onEscape() {
emit('esc'); emit('esc');

View file

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'; import { computed, nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue';
import { onClickOutside } from '@vueuse/core';
import ExpressionFunctionIcon from '@/components/ExpressionFunctionIcon.vue'; import ExpressionFunctionIcon from '@/components/ExpressionFunctionIcon.vue';
import InlineExpressionEditorInput from '@/components/InlineExpressionEditor/InlineExpressionEditorInput.vue'; import InlineExpressionEditorInput from '@/components/InlineExpressionEditor/InlineExpressionEditorInput.vue';
@ -21,6 +22,7 @@ const segments = ref<Segment[]>([]);
const editorState = ref<EditorState>(); const editorState = ref<EditorState>();
const selection = ref<SelectionRange>(); const selection = ref<SelectionRange>();
const inlineInput = ref<InstanceType<typeof InlineExpressionEditorInput>>(); const inlineInput = ref<InstanceType<typeof InlineExpressionEditorInput>>();
const container = ref<HTMLDivElement>();
type Props = { type Props = {
path: string; path: string;
@ -156,15 +158,13 @@ watch(isDragging, (newIsDragging) => {
} }
}); });
onClickOutside(container, (event) => onBlur(event));
defineExpose({ focus }); defineExpose({ focus });
</script> </script>
<template> <template>
<div <div ref="container" :class="$style['expression-parameter-input']" @keydown.tab="onBlur">
v-on-click-outside="onBlur"
:class="$style['expression-parameter-input']"
@keydown.tab="onBlur"
>
<div <div
:class="[ :class="[
$style['all-sections'], $style['all-sections'],

View file

@ -17,8 +17,9 @@
}" }"
v-text="`${connection.displayName}${connection.required ? ' *' : ''}`" v-text="`${connection.displayName}${connection.required ? ' *' : ''}`"
/> />
<OnClickOutside @trigger="expandConnectionGroup(connection.type, false)">
<div <div
v-on-click-outside="() => expandConnectionGroup(connection.type, false)" ref="connectedNodesWrapper"
:class="{ :class="{
[$style.connectedNodesWrapper]: true, [$style.connectedNodesWrapper]: true,
[$style.connectedNodesWrapperExpanded]: expandedGroups.includes(connection.type), [$style.connectedNodesWrapperExpanded]: expandedGroups.includes(connection.type),
@ -28,7 +29,9 @@
> >
<div <div
v-if=" v-if="
connectedNodes[connection.type].length >= 1 ? connection.maxConnections !== 1 : true connectedNodes[connection.type].length >= 1
? connection.maxConnections !== 1
: true
" "
:class="{ :class="{
[$style.plusButton]: true, [$style.plusButton]: true,
@ -112,6 +115,7 @@
</div> </div>
</div> </div>
</div> </div>
</OnClickOutside>
</div> </div>
</div> </div>
</div> </div>
@ -129,6 +133,7 @@ import NodeIcon from '@/components/NodeIcon.vue';
import TitledList from '@/components/TitledList.vue'; import TitledList from '@/components/TitledList.vue';
import type { ConnectionTypes, INodeInputConfiguration, INodeTypeDescription } from 'n8n-workflow'; import type { ConnectionTypes, INodeInputConfiguration, INodeTypeDescription } from 'n8n-workflow';
import { useDebounce } from '@/composables/useDebounce'; import { useDebounce } from '@/composables/useDebounce';
import { OnClickOutside } from '@vueuse/components';
interface Props { interface Props {
rootNode: INodeUi; rootNode: INodeUi;

View file

@ -1,7 +1,7 @@
import { within } from '@testing-library/vue'; import { within } from '@testing-library/vue';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import { getDropdownItems } from '@/__tests__/utils'; import { getDropdownItems, getSelectedDropdownValue } from '@/__tests__/utils';
import { createProjectListItem, createProjectSharingData } from '@/__tests__/data/projects'; import { createProjectListItem, createProjectSharingData } from '@/__tests__/data/projects';
import ProjectSharing from '@/components/Projects/ProjectSharing.vue'; import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
@ -112,7 +112,6 @@ describe('ProjectSharing', () => {
expect(queryByTestId('project-sharing-owner')).not.toBeInTheDocument(); expect(queryByTestId('project-sharing-owner')).not.toBeInTheDocument();
const projectSelect = getByTestId('project-sharing-select'); const projectSelect = getByTestId('project-sharing-select');
const projectSelectInput = projectSelect.querySelector('input') as HTMLInputElement;
// Get the dropdown items // Get the dropdown items
let projectSelectDropdownItems = await getDropdownItems(projectSelect); let projectSelectDropdownItems = await getDropdownItems(projectSelect);
@ -123,11 +122,13 @@ describe('ProjectSharing', () => {
expect(queryByTestId('project-sharing-list-item')).not.toBeInTheDocument(); expect(queryByTestId('project-sharing-list-item')).not.toBeInTheDocument();
projectSelectDropdownItems = await getDropdownItems(projectSelect); projectSelectDropdownItems = await getDropdownItems(projectSelect);
expect(projectSelectDropdownItems).toHaveLength(3); expect(projectSelectDropdownItems).toHaveLength(3);
expect(projectSelectDropdownItems[0].textContent).toContain(projectSelectInput.value);
const selectedValue = await getSelectedDropdownValue(projectSelectDropdownItems);
expect(selectedValue).toBeTruthy();
expect(emitted()['update:modelValue']).toEqual([ expect(emitted()['update:modelValue']).toEqual([
[ [
expect.objectContaining({ expect.objectContaining({
name: projectSelectInput.value, name: selectedValue,
}), }),
], ],
]); ]);
@ -136,12 +137,13 @@ describe('ProjectSharing', () => {
await userEvent.click(projectSelectDropdownItems[1]); await userEvent.click(projectSelectDropdownItems[1]);
projectSelectDropdownItems = await getDropdownItems(projectSelect); projectSelectDropdownItems = await getDropdownItems(projectSelect);
expect(projectSelectDropdownItems).toHaveLength(3); expect(projectSelectDropdownItems).toHaveLength(3);
expect(projectSelectDropdownItems[1].textContent).toContain(projectSelectInput.value); const newSelectedValue = await getSelectedDropdownValue(projectSelectDropdownItems);
expect(newSelectedValue).toBeTruthy();
expect(emitted()['update:modelValue']).toEqual([ expect(emitted()['update:modelValue']).toEqual([
expect.any(Array), expect.any(Array),
[ [
expect.objectContaining({ expect.objectContaining({
name: projectSelectInput.value, name: newSelectedValue,
}), }),
], ],
]); ]);

View file

@ -4,9 +4,9 @@
class="resource-locator" class="resource-locator"
:data-test-id="`resource-locator-${parameter.name}`" :data-test-id="`resource-locator-${parameter.name}`"
> >
<OnClickOutside @trigger="hideResourceDropdown">
<ResourceLocatorDropdown <ResourceLocatorDropdown
ref="dropdown" ref="dropdown"
v-on-click-outside="hideResourceDropdown"
:model-value="modelValue ? modelValue.value : ''" :model-value="modelValue ? modelValue.value : ''"
:show="resourceDropdownVisible" :show="resourceDropdownVisible"
:filterable="isSearchable" :filterable="isSearchable"
@ -140,6 +140,7 @@
</div> </div>
</div> </div>
</ResourceLocatorDropdown> </ResourceLocatorDropdown>
</OnClickOutside>
</div> </div>
</template> </template>
@ -176,6 +177,7 @@ import { useDebounce } from '@/composables/useDebounce';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { ndvEventBus } from '@/event-bus'; import { ndvEventBus } from '@/event-bus';
import { OnClickOutside } from '@vueuse/components';
interface IResourceLocatorQuery { interface IResourceLocatorQuery {
results: INodeListSearchItems[]; results: INodeListSearchItems[];
@ -191,6 +193,7 @@ export default defineComponent({
ExpressionParameterInput, ExpressionParameterInput,
ParameterIssues, ParameterIssues,
ResourceLocatorDropdown, ResourceLocatorDropdown,
OnClickOutside,
}, },
props: { props: {
parameter: { parameter: {

View file

@ -47,6 +47,7 @@
<div <div
v-show="showActions" v-show="showActions"
ref="stickOptions"
:class="{ 'sticky-options': true, 'no-select-on-click': true, 'force-show': forceActions }" :class="{ 'sticky-options': true, 'no-select-on-click': true, 'force-show': forceActions }"
> >
<div <div
@ -58,7 +59,6 @@
<font-awesome-icon icon="trash" /> <font-awesome-icon icon="trash" />
</div> </div>
<n8n-popover <n8n-popover
v-on-click-outside="() => setColorPopoverVisible(false)"
effect="dark" effect="dark"
trigger="click" trigger="click"
placement="top" placement="top"
@ -109,6 +109,8 @@ import { defineComponent, ref } from 'vue';
import type { PropType, StyleValue } from 'vue'; import type { PropType, StyleValue } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { onClickOutside } from '@vueuse/core';
import { isNumber, isString } from '@/utils/typeGuards'; import { isNumber, isString } from '@/utils/typeGuards';
import type { import type {
INodeUi, INodeUi,
@ -178,6 +180,9 @@ export default defineComponent({
const toast = useToast(); const toast = useToast();
const forceActions = ref(false); const forceActions = ref(false);
const isColorPopoverVisible = ref(false); const isColorPopoverVisible = ref(false);
const stickOptions = ref<HTMLElement>();
const setForceActions = (value: boolean) => { const setForceActions = (value: boolean) => {
forceActions.value = value; forceActions.value = value;
}; };
@ -200,6 +205,8 @@ export default defineComponent({
emit: emit as (event: string, ...args: unknown[]) => void, emit: emit as (event: string, ...args: unknown[]) => void,
}); });
onClickOutside(stickOptions, () => setColorPopoverVisible(false));
return { return {
deviceSupport, deviceSupport,
toast, toast,
@ -209,6 +216,7 @@ export default defineComponent({
setForceActions, setForceActions,
isColorPopoverVisible, isColorPopoverVisible,
setColorPopoverVisible, setColorPopoverVisible,
stickOptions,
}; };
}, },
data() { data() {

View file

@ -1,9 +1,5 @@
<template> <template>
<div <div ref="container" :class="{ 'tags-container': true, focused }" @keydown.stop>
v-on-click-outside="onClickOutside"
:class="{ 'tags-container': true, focused }"
@keydown.stop
>
<n8n-select <n8n-select
ref="selectRef" ref="selectRef"
:teleported="true" :teleported="true"
@ -73,6 +69,7 @@ import { useTagsStore } from '@/stores/tags.store';
import type { EventBus, N8nOption, N8nSelect } from 'n8n-design-system'; import type { EventBus, N8nOption, N8nSelect } from 'n8n-design-system';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { onClickOutside } from '@vueuse/core';
type SelectRef = InstanceType<typeof N8nSelect>; type SelectRef = InstanceType<typeof N8nSelect>;
type TagRef = InstanceType<typeof N8nOption>; type TagRef = InstanceType<typeof N8nOption>;
@ -116,6 +113,8 @@ export default defineComponent({
const focused = ref(false); const focused = ref(false);
const preventUpdate = ref(false); const preventUpdate = ref(false);
const container = ref<HTMLDivElement | undefined>();
const allTags = computed<ITag[]>(() => { const allTags = computed<ITag[]>(() => {
return tagsStore.allTags; return tagsStore.allTags;
}); });
@ -252,18 +251,13 @@ export default defineComponent({
}); });
} }
function onClickOutside(e: Event) { onClickOutside(
const tagsDropdown = document.querySelector('.tags-dropdown'); container,
const tagsModal = document.querySelector('#tags-manager-modal'); () => {
const clickInsideTagsDropdowns =
tagsDropdown?.contains(e.target as Node) ?? tagsDropdown === e.target;
const clickInsideTagsModal = tagsModal?.contains(e.target as Node) ?? tagsModal === e.target;
if (!clickInsideTagsDropdowns && !clickInsideTagsModal && e.type === 'click') {
emit('blur'); emit('blur');
} },
} { ignore: ['.tags-dropdown', '#tags-manager-modal'] },
);
return { return {
i18n, i18n,
@ -285,7 +279,7 @@ export default defineComponent({
filterOptions, filterOptions,
onVisibleChange, onVisibleChange,
onRemoveTag, onRemoveTag,
onClickOutside, container,
...useToast(), ...useToast(),
}; };
}, },

View file

@ -116,8 +116,10 @@ describe('WorkflowLMChatModal', () => {
await fireEvent.click(chatSendButton); await fireEvent.click(chatSendButton);
} }
await waitFor(() => expect(chatDialog.querySelectorAll('.chat-message')).toHaveLength(1)); await waitFor(() =>
expect(chatDialog.querySelectorAll('.chat-message-from-user')).toHaveLength(1),
);
expect(chatDialog.querySelector('.chat-message')).toHaveTextContent('Hello!'); expect(chatDialog.querySelector('.chat-message-from-user')).toHaveTextContent('Hello!');
}); });
}); });

View file

@ -1,10 +1,8 @@
import type { Plugin } from 'vue'; import type { Plugin } from 'vue';
import VueTouchEvents from 'vue3-touch-events'; import VueTouchEvents from 'vue3-touch-events';
import { vOnClickOutside } from '@vueuse/components';
export const GlobalDirectivesPlugin: Plugin = { export const GlobalDirectivesPlugin: Plugin = {
install(app) { install(app) {
app.use(VueTouchEvents); app.use(VueTouchEvents);
app.directive('on-click-outside', vOnClickOutside);
}, },
}; };

View file

@ -105,7 +105,7 @@ describe('SettingsSourceControl', () => {
expect(saveSettingsButton).toBeDisabled(); expect(saveSettingsButton).toBeDisabled();
const branchSelect = getByTestId('source-control-branch-select'); const branchSelect = getByTestId('source-control-branch-select');
await userEvent.click(within(branchSelect).getByRole('textbox')); await userEvent.click(within(branchSelect).getByRole('combobox'));
await waitFor(() => expect(getByText('main')).toBeVisible()); await waitFor(() => expect(getByText('main')).toBeVisible());
await userEvent.click(getByText('main')); await userEvent.click(getByText('main'));
@ -137,7 +137,7 @@ describe('SettingsSourceControl', () => {
expect(refreshSshKeyButton).toBeVisible(); expect(refreshSshKeyButton).toBeVisible();
}); });
await userEvent.click(within(sshKeyTypeSelect).getByRole('textbox')); await userEvent.click(within(sshKeyTypeSelect).getByRole('combobox'));
await waitFor(() => expect(getByText('RSA')).toBeVisible()); await waitFor(() => expect(getByText('RSA')).toBeVisible());
await userEvent.click(getByText('RSA')); await userEvent.click(getByText('RSA'));
await userEvent.click(refreshSshKeyButton); await userEvent.click(refreshSshKeyButton);

View file

@ -1036,8 +1036,8 @@ importers:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3(@fortawesome/fontawesome-svg-core@1.2.36)(vue@3.4.21(typescript@5.5.2)) version: 3.0.3(@fortawesome/fontawesome-svg-core@1.2.36)(vue@3.4.21(typescript@5.5.2))
element-plus: element-plus:
specifier: ^2.3.6 specifier: 2.4.3
version: 2.3.6(vue@3.4.21(typescript@5.5.2)) version: 2.4.3(vue@3.4.21(typescript@5.5.2))
markdown-it: markdown-it:
specifier: ^13.0.2 specifier: ^13.0.2
version: 13.0.2 version: 13.0.2
@ -3065,8 +3065,8 @@ packages:
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
'@element-plus/icons-vue@2.1.0': '@element-plus/icons-vue@2.3.1':
resolution: {integrity: sha512-PSBn3elNoanENc1vnCfh+3WA9fimRC7n+fWkf3rE5jvv+aBohNHABC/KAR5KWPecxWxDTVT1ERpRbOMRcOV/vA==} resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==}
peerDependencies: peerDependencies:
vue: ^3.2.0 vue: ^3.2.0
@ -7097,9 +7097,6 @@ packages:
dayjs@1.11.10: dayjs@1.11.10:
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
dayjs@1.11.6:
resolution: {integrity: sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==}
de-indent@1.0.2: de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
@ -7409,8 +7406,8 @@ packages:
electron-to-chromium@1.4.703: electron-to-chromium@1.4.703:
resolution: {integrity: sha512-094ZZC4nHXPKl/OwPinSMtLN9+hoFkdfQGKnvXbY+3WEAYtVDpz9UhJIViiY6Zb8agvqxiaJzNG9M+pRZWvSZw==} resolution: {integrity: sha512-094ZZC4nHXPKl/OwPinSMtLN9+hoFkdfQGKnvXbY+3WEAYtVDpz9UhJIViiY6Zb8agvqxiaJzNG9M+pRZWvSZw==}
element-plus@2.3.6: element-plus@2.4.3:
resolution: {integrity: sha512-GLz0pXUYI2zRfIgyI6W7SWmHk6dSEikP9yR++hsQUyy63+WjutoiGpA3SZD4cGPSXUzRFeKfVr8CnYhK5LqXZw==} resolution: {integrity: sha512-b3q26j+lM4SBqiyzw8HybybGnP2pk4MWgrnzzzYW5qKQUgV6EG1Zg7nMCfgCVccI8tNvZoTiUHb2mFaiB9qT8w==}
peerDependencies: peerDependencies:
vue: ^3.2.0 vue: ^3.2.0
@ -12855,17 +12852,6 @@ packages:
'@vue/composition-api': '@vue/composition-api':
optional: true optional: true
vue-demi@0.14.6:
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue-demi@0.14.8: vue-demi@0.14.8:
resolution: {integrity: sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==} resolution: {integrity: sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -15596,7 +15582,7 @@ snapshots:
'@discoveryjs/json-ext@0.5.7': {} '@discoveryjs/json-ext@0.5.7': {}
'@element-plus/icons-vue@2.1.0(vue@3.4.21(typescript@5.5.2))': '@element-plus/icons-vue@2.3.1(vue@3.4.21(typescript@5.5.2))':
dependencies: dependencies:
vue: 3.4.21(typescript@5.5.2) vue: 3.4.21(typescript@5.5.2)
@ -19251,7 +19237,7 @@ snapshots:
'@types/web-bluetooth': 0.0.16 '@types/web-bluetooth': 0.0.16
'@vueuse/metadata': 9.13.0 '@vueuse/metadata': 9.13.0
'@vueuse/shared': 9.13.0(vue@3.4.21(typescript@5.5.2)) '@vueuse/shared': 9.13.0(vue@3.4.21(typescript@5.5.2))
vue-demi: 0.14.6(vue@3.4.21(typescript@5.5.2)) vue-demi: 0.14.8(vue@3.4.21(typescript@5.5.2))
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
@ -19269,7 +19255,7 @@ snapshots:
'@vueuse/shared@9.13.0(vue@3.4.21(typescript@5.5.2))': '@vueuse/shared@9.13.0(vue@3.4.21(typescript@5.5.2))':
dependencies: dependencies:
vue-demi: 0.14.6(vue@3.4.21(typescript@5.5.2)) vue-demi: 0.14.8(vue@3.4.21(typescript@5.5.2))
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
@ -20637,8 +20623,6 @@ snapshots:
dayjs@1.11.10: {} dayjs@1.11.10: {}
dayjs@1.11.6: {}
de-indent@1.0.2: {} de-indent@1.0.2: {}
debug@2.6.9: debug@2.6.9:
@ -20941,17 +20925,17 @@ snapshots:
electron-to-chromium@1.4.703: {} electron-to-chromium@1.4.703: {}
element-plus@2.3.6(vue@3.4.21(typescript@5.5.2)): element-plus@2.4.3(vue@3.4.21(typescript@5.5.2)):
dependencies: dependencies:
'@ctrl/tinycolor': 3.6.0 '@ctrl/tinycolor': 3.6.0
'@element-plus/icons-vue': 2.1.0(vue@3.4.21(typescript@5.5.2)) '@element-plus/icons-vue': 2.3.1(vue@3.4.21(typescript@5.5.2))
'@floating-ui/dom': 1.4.5 '@floating-ui/dom': 1.4.5
'@popperjs/core': '@sxzz/popperjs-es@2.11.7' '@popperjs/core': '@sxzz/popperjs-es@2.11.7'
'@types/lodash': 4.14.195 '@types/lodash': 4.14.195
'@types/lodash-es': 4.17.6 '@types/lodash-es': 4.17.6
'@vueuse/core': 9.13.0(vue@3.4.21(typescript@5.5.2)) '@vueuse/core': 9.13.0(vue@3.4.21(typescript@5.5.2))
async-validator: 4.2.5 async-validator: 4.2.5
dayjs: 1.11.6 dayjs: 1.11.10
escape-html: 1.0.3 escape-html: 1.0.3
lodash: 4.17.21 lodash: 4.17.21
lodash-es: 4.17.21 lodash-es: 4.17.21
@ -27352,10 +27336,6 @@ snapshots:
dependencies: dependencies:
vue: 3.4.21(typescript@5.5.2) vue: 3.4.21(typescript@5.5.2)
vue-demi@0.14.6(vue@3.4.21(typescript@5.5.2)):
dependencies:
vue: 3.4.21(typescript@5.5.2)
vue-demi@0.14.8(vue@3.4.21(typescript@5.5.2)): vue-demi@0.14.8(vue@3.4.21(typescript@5.5.2)):
dependencies: dependencies:
vue: 3.4.21(typescript@5.5.2) vue: 3.4.21(typescript@5.5.2)