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');
});
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', () => {
beforeEach(() => {
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', () => {
wf.getters.createTagButton().click();
wf.actions.addTags(TEST_TAGS);
wf.getters.nthTagPill(1).click();
wf.getters.workflowTagsContainer().click();
wf.getters.tagsInDropdown().filter('.selected').first().click();
cy.get('body').click(0, 0);
wf.getters.workflowTags().click();
@ -79,7 +79,7 @@ describe('Workflow tags', () => {
wf.actions.addTags(TEST_TAGS);
cy.get('body').click(0, 0);
wf.getters.workflowTags().click();
wf.getters.tagsDropdown().find('input:focus').type(NON_EXISTING_TAG);
wf.getters.workflowTagsInput().type(NON_EXISTING_TAG);
getVisibleSelect()
.find('li')

View file

@ -112,13 +112,13 @@ describe('Credentials', () => {
workflowPage.getters.nodeCredentialsSelect().should('have.length', 2);
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
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
cy.get('body').type('{esc}');
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
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
});

View file

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

View file

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

View file

@ -44,7 +44,7 @@
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.3",
"element-plus": "^2.3.6",
"element-plus": "2.4.3",
"markdown-it": "^13.0.2",
"markdown-it-emoji": "^2.0.2",
"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"
>
<input
aria-hidden="false"
class="el-checkbox__original"
type="checkbox"
value="Checkbox"
@ -71,7 +70,6 @@ exports[`components > N8nCheckbox > should render with child 1`] = `
class="el-checkbox__input"
>
<input
aria-hidden="false"
class="el-checkbox__original"
type="checkbox"
/>
@ -106,7 +104,6 @@ exports[`components > N8nCheckbox > should render with label 1`] = `
class="el-checkbox__input"
>
<input
aria-hidden="false"
class="el-checkbox__original"
type="checkbox"
value="Checkbox"
@ -164,7 +161,6 @@ exports[`components > N8nCheckbox > should render without label and child conten
class="el-checkbox__input"
>
<input
aria-hidden="false"
class="el-checkbox__original"
type="checkbox"
/>

View file

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

View file

@ -1,6 +1,7 @@
import { render } from '@testing-library/vue';
import N8nDatatable from '../Datatable.vue';
import { rows, columns } from './data';
import { removeDynamicAttributes } from 'n8n-design-system/utils';
const stubs = [
'n8n-option',
@ -33,6 +34,7 @@ describe('components', () => {
expect(wrapper.container.querySelectorAll('tbody tr td').length).toEqual(
columns.length * rowsPerPage,
);
removeDynamicAttributes(wrapper.container);
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="select-trigger el-tooltip__trigger el-tooltip__trigger">
<!--v-if-->
<!-- fix: https://github.com/element-plus/element-plus/issues/11415 -->
<!--v-if-->
<div class="el-input el-input--small el-input--suffix">
<!-- input -->
@ -121,7 +120,7 @@ exports[`components > N8nDatatable > should render correctly 1`] = `
<!--v-if-->
<div class="el-input__wrapper">
<!-- 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 -->
<!--v-if-->
</div>

View file

@ -3,6 +3,7 @@ import { render, waitFor, within } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import N8nSelect from '../Select.vue';
import N8nOption from '../../N8nOption/Option.vue';
import { removeDynamicAttributes } from 'n8n-design-system/utils';
describe('components', () => {
describe('N8nSelect', () => {
@ -21,6 +22,7 @@ describe('components', () => {
],
},
});
removeDynamicAttributes(wrapper.container);
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="select-trigger el-tooltip__trigger el-tooltip__trigger">
<!--v-if-->
<!-- fix: https://github.com/element-plus/element-plus/issues/11415 -->
<!--v-if-->
<div class="el-input el-input--large el-input--suffix">
<!-- input -->
@ -14,7 +13,7 @@ exports[`components > N8nSelect > should render correctly 1`] = `
<!--v-if-->
<div class="el-input__wrapper">
<!-- 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 -->
<!--v-if-->
</div>

View file

@ -4,3 +4,4 @@ export * from './markdown';
export * from './typeguards';
export * from './uid';
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) => {
await userEvent.click(within(dropdownTriggerParent).getByRole('textbox'));
await userEvent.click(within(dropdownTriggerParent).getByRole('combobox'));
const selectTrigger = dropdownTriggerParent.querySelector(
'.select-trigger[aria-describedby]',
) as HTMLElement;
@ -84,3 +84,9 @@ export const getDropdownItems = async (dropdownTriggerParent: HTMLElement) => {
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 { N8nActionDropdown } from 'n8n-design-system';
import { watch, ref } from 'vue';
import { onClickOutside } from '@vueuse/core';
const contextMenu = useContextMenu();
const { position, isOpen, actions, target } = contextMenu;
const dropdown = ref<InstanceType<typeof N8nActionDropdown>>();
const emit = defineEmits<{ action: [action: ContextMenuAction, nodeIds: string[]] }>();
const container = ref<HTMLDivElement>();
watch(
isOpen,
@ -26,7 +28,7 @@ function onActionSelect(item: string) {
emit('action', action, contextMenu.targetNodeIds.value);
}
function onClickOutside(event: MouseEvent) {
function closeMenu(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
contextMenu.close();
@ -37,12 +39,14 @@ function onVisibleChange(open: boolean) {
contextMenu.close();
}
}
onClickOutside(container, closeMenu);
</script>
<template>
<Teleport v-if="isOpen" to="body">
<div
v-on-click-outside="onClickOutside"
ref="container"
:class="$style.contextMenu"
:style="{
left: `${position[0]}px`,

View file

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

View file

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

View file

@ -17,101 +17,105 @@
}"
v-text="`${connection.displayName}${connection.required ? ' *' : ''}`"
/>
<div
v-on-click-outside="() => expandConnectionGroup(connection.type, false)"
:class="{
[$style.connectedNodesWrapper]: true,
[$style.connectedNodesWrapperExpanded]: expandedGroups.includes(connection.type),
}"
:style="`--nodes-length: ${connectedNodes[connection.type].length}`"
@click="expandConnectionGroup(connection.type, true)"
>
<OnClickOutside @trigger="expandConnectionGroup(connection.type, false)">
<div
v-if="
connectedNodes[connection.type].length >= 1 ? connection.maxConnections !== 1 : true
"
ref="connectedNodesWrapper"
:class="{
[$style.plusButton]: true,
[$style.hasIssues]: hasInputIssues(connection.type),
}"
@click="onPlusClick(connection.type)"
>
<n8n-tooltip
placement="top"
:teleported="true"
:offset="10"
:show-after="300"
:disabled="
shouldShowConnectionTooltip(connection.type) &&
connectedNodes[connection.type].length >= 1
"
>
<template #content>
Add {{ connection.displayName }}
<template v-if="hasInputIssues(connection.type)">
<TitledList
:title="`${$locale.baseText('node.issues')}:`"
:items="nodeInputIssues[connection.type]"
/>
</template>
</template>
<n8n-icon-button
size="medium"
icon="plus"
type="tertiary"
:data-test-id="`add-subnode-${connection.type}`"
/>
</n8n-tooltip>
</div>
<div
v-if="connectedNodes[connection.type].length > 0"
:class="{
[$style.connectedNodes]: true,
[$style.connectedNodesMultiple]: connectedNodes[connection.type].length > 1,
[$style.connectedNodesWrapper]: true,
[$style.connectedNodesWrapperExpanded]: expandedGroups.includes(connection.type),
}"
:style="`--nodes-length: ${connectedNodes[connection.type].length}`"
@click="expandConnectionGroup(connection.type, true)"
>
<div
v-for="(node, index) in connectedNodes[connection.type]"
:key="node.node.name"
:class="{ [$style.nodeWrapper]: true, [$style.hasIssues]: node.issues }"
data-test-id="floating-subnode"
:data-node-name="node.node.name"
:style="`--node-index: ${index}`"
v-if="
connectedNodes[connection.type].length >= 1
? connection.maxConnections !== 1
: true
"
:class="{
[$style.plusButton]: true,
[$style.hasIssues]: hasInputIssues(connection.type),
}"
@click="onPlusClick(connection.type)"
>
<n8n-tooltip
:key="node.node.name"
placement="top"
:teleported="true"
:offset="10"
:show-after="300"
:disabled="shouldShowConnectionTooltip(connection.type)"
:disabled="
shouldShowConnectionTooltip(connection.type) &&
connectedNodes[connection.type].length >= 1
"
>
<template #content>
{{ node.node.name }}
<template v-if="node.issues">
Add {{ connection.displayName }}
<template v-if="hasInputIssues(connection.type)">
<TitledList
:title="`${$locale.baseText('node.issues')}:`"
:items="node.issues"
:items="nodeInputIssues[connection.type]"
/>
</template>
</template>
<div
:class="$style.connectedNode"
@click="onNodeClick(node.node.name, connection.type)"
>
<NodeIcon
:node-type="node.nodeType"
:node-name="node.node.name"
tooltip-position="top"
:size="20"
circle
/>
</div>
<n8n-icon-button
size="medium"
icon="plus"
type="tertiary"
:data-test-id="`add-subnode-${connection.type}`"
/>
</n8n-tooltip>
</div>
<div
v-if="connectedNodes[connection.type].length > 0"
:class="{
[$style.connectedNodes]: true,
[$style.connectedNodesMultiple]: connectedNodes[connection.type].length > 1,
}"
>
<div
v-for="(node, index) in connectedNodes[connection.type]"
:key="node.node.name"
:class="{ [$style.nodeWrapper]: true, [$style.hasIssues]: node.issues }"
data-test-id="floating-subnode"
:data-node-name="node.node.name"
:style="`--node-index: ${index}`"
>
<n8n-tooltip
:key="node.node.name"
placement="top"
:teleported="true"
:offset="10"
:show-after="300"
:disabled="shouldShowConnectionTooltip(connection.type)"
>
<template #content>
{{ node.node.name }}
<template v-if="node.issues">
<TitledList
:title="`${$locale.baseText('node.issues')}:`"
:items="node.issues"
/>
</template>
</template>
<div
:class="$style.connectedNode"
@click="onNodeClick(node.node.name, connection.type)"
>
<NodeIcon
:node-type="node.nodeType"
:node-name="node.node.name"
tooltip-position="top"
:size="20"
circle
/>
</div>
</n8n-tooltip>
</div>
</div>
</div>
</div>
</OnClickOutside>
</div>
</div>
</div>
@ -129,6 +133,7 @@ import NodeIcon from '@/components/NodeIcon.vue';
import TitledList from '@/components/TitledList.vue';
import type { ConnectionTypes, INodeInputConfiguration, INodeTypeDescription } from 'n8n-workflow';
import { useDebounce } from '@/composables/useDebounce';
import { OnClickOutside } from '@vueuse/components';
interface Props {
rootNode: INodeUi;

View file

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

View file

@ -4,142 +4,143 @@
class="resource-locator"
:data-test-id="`resource-locator-${parameter.name}`"
>
<ResourceLocatorDropdown
ref="dropdown"
v-on-click-outside="hideResourceDropdown"
:model-value="modelValue ? modelValue.value : ''"
:show="resourceDropdownVisible"
:filterable="isSearchable"
:filter-required="requiresSearchFilter"
:resources="currentQueryResults"
:loading="currentQueryLoading"
:filter="searchFilter"
:has-more="currentQueryHasMore"
:error-view="currentQueryError"
:width="width"
:event-bus="eventBus"
@update:model-value="onListItemSelected"
@filter="onSearchFilter"
@load-more="loadResourcesDebounced"
>
<template #error>
<div :class="$style.error" data-test-id="rlc-error-container">
<n8n-text color="text-dark" align="center" tag="div">
{{ $locale.baseText('resourceLocator.mode.list.error.title') }}
</n8n-text>
<n8n-text v-if="hasCredential || credentialsNotSet" size="small" color="text-base">
{{ $locale.baseText('resourceLocator.mode.list.error.description.part1') }}
<a v-if="credentialsNotSet" @click="createNewCredential">{{
$locale.baseText('resourceLocator.mode.list.error.description.part2.noCredentials')
}}</a>
<a v-else-if="hasCredential" @click="openCredential">{{
$locale.baseText('resourceLocator.mode.list.error.description.part2.hasCredentials')
}}</a>
</n8n-text>
</div>
</template>
<div
:class="{
[$style.resourceLocator]: true,
[$style.multipleModes]: hasMultipleModes,
}"
<OnClickOutside @trigger="hideResourceDropdown">
<ResourceLocatorDropdown
ref="dropdown"
:model-value="modelValue ? modelValue.value : ''"
:show="resourceDropdownVisible"
:filterable="isSearchable"
:filter-required="requiresSearchFilter"
:resources="currentQueryResults"
:loading="currentQueryLoading"
:filter="searchFilter"
:has-more="currentQueryHasMore"
:error-view="currentQueryError"
:width="width"
:event-bus="eventBus"
@update:model-value="onListItemSelected"
@filter="onSearchFilter"
@load-more="loadResourcesDebounced"
>
<div :class="$style.background"></div>
<div v-if="hasMultipleModes" :class="$style.modeSelector">
<n8n-select
:model-value="selectedMode"
:size="inputSize"
:disabled="isReadOnly"
:placeholder="$locale.baseText('resourceLocator.modeSelector.placeholder')"
data-test-id="rlc-mode-selector"
@update:model-value="onModeSelected"
>
<n8n-option
v-for="mode in parameter.modes"
:key="mode.name"
:value="mode.name"
:label="getModeLabel(mode)"
:disabled="isValueExpression && mode.name === 'list'"
:title="
isValueExpression && mode.name === 'list'
? $locale.baseText('resourceLocator.mode.list.disabled.title')
: ''
"
<template #error>
<div :class="$style.error" data-test-id="rlc-error-container">
<n8n-text color="text-dark" align="center" tag="div">
{{ $locale.baseText('resourceLocator.mode.list.error.title') }}
</n8n-text>
<n8n-text v-if="hasCredential || credentialsNotSet" size="small" color="text-base">
{{ $locale.baseText('resourceLocator.mode.list.error.description.part1') }}
<a v-if="credentialsNotSet" @click="createNewCredential">{{
$locale.baseText('resourceLocator.mode.list.error.description.part2.noCredentials')
}}</a>
<a v-else-if="hasCredential" @click="openCredential">{{
$locale.baseText('resourceLocator.mode.list.error.description.part2.hasCredentials')
}}</a>
</n8n-text>
</div>
</template>
<div
:class="{
[$style.resourceLocator]: true,
[$style.multipleModes]: hasMultipleModes,
}"
>
<div :class="$style.background"></div>
<div v-if="hasMultipleModes" :class="$style.modeSelector">
<n8n-select
:model-value="selectedMode"
:size="inputSize"
:disabled="isReadOnly"
:placeholder="$locale.baseText('resourceLocator.modeSelector.placeholder')"
data-test-id="rlc-mode-selector"
@update:model-value="onModeSelected"
>
{{ getModeLabel(mode) }}
</n8n-option>
</n8n-select>
</div>
<div :class="$style.inputContainer" data-test-id="rlc-input-container">
<DraggableTarget
type="mapping"
:disabled="hasOnlyListMode"
:sticky="true"
:sticky-offset="isValueExpression ? [26, 3] : [3, 3]"
@drop="onDrop"
>
<template #default="{ droppable, activeDrop }">
<div
:class="{
[$style.listModeInputContainer]: isListMode,
[$style.droppable]: droppable,
[$style.activeDrop]: activeDrop,
}"
@keydown.stop="onKeyDown"
<n8n-option
v-for="mode in parameter.modes"
:key="mode.name"
:value="mode.name"
:label="getModeLabel(mode)"
:disabled="isValueExpression && mode.name === 'list'"
:title="
isValueExpression && mode.name === 'list'
? $locale.baseText('resourceLocator.mode.list.disabled.title')
: ''
"
>
<ExpressionParameterInput
v-if="isValueExpression || forceShowExpression"
ref="input"
:model-value="expressionDisplayValue"
:path="path"
:rows="3"
@update:model-value="onInputChange"
@modal-opener-click="$emit('modalOpenerClick')"
/>
<n8n-input
v-else
ref="input"
:class="{ [$style.selectInput]: isListMode }"
:size="inputSize"
:model-value="valueToDisplay"
:disabled="isReadOnly"
:readonly="isListMode"
:title="displayTitle"
:placeholder="inputPlaceholder"
type="text"
data-test-id="rlc-input"
@update:model-value="onInputChange"
@focus="onInputFocus"
@blur="onInputBlur"
{{ getModeLabel(mode) }}
</n8n-option>
</n8n-select>
</div>
<div :class="$style.inputContainer" data-test-id="rlc-input-container">
<DraggableTarget
type="mapping"
:disabled="hasOnlyListMode"
:sticky="true"
:sticky-offset="isValueExpression ? [26, 3] : [3, 3]"
@drop="onDrop"
>
<template #default="{ droppable, activeDrop }">
<div
:class="{
[$style.listModeInputContainer]: isListMode,
[$style.droppable]: droppable,
[$style.activeDrop]: activeDrop,
}"
@keydown.stop="onKeyDown"
>
<template v-if="isListMode" #suffix>
<i
:class="{
['el-input__icon']: true,
['el-icon-arrow-down']: true,
[$style.selectIcon]: true,
[$style.isReverse]: resourceDropdownVisible,
}"
/>
</template>
</n8n-input>
</div>
</template>
</DraggableTarget>
<ParameterIssues
v-if="parameterIssues && parameterIssues.length"
:issues="parameterIssues"
:class="$style['parameter-issues']"
/>
<div v-else-if="urlValue" :class="$style.openResourceLink">
<n8n-link theme="text" @click.stop="openResource(urlValue)">
<font-awesome-icon icon="external-link-alt" :title="getLinkAlt(valueToDisplay)" />
</n8n-link>
<ExpressionParameterInput
v-if="isValueExpression || forceShowExpression"
ref="input"
:model-value="expressionDisplayValue"
:path="path"
:rows="3"
@update:model-value="onInputChange"
@modal-opener-click="$emit('modalOpenerClick')"
/>
<n8n-input
v-else
ref="input"
:class="{ [$style.selectInput]: isListMode }"
:size="inputSize"
:model-value="valueToDisplay"
:disabled="isReadOnly"
:readonly="isListMode"
:title="displayTitle"
:placeholder="inputPlaceholder"
type="text"
data-test-id="rlc-input"
@update:model-value="onInputChange"
@focus="onInputFocus"
@blur="onInputBlur"
>
<template v-if="isListMode" #suffix>
<i
:class="{
['el-input__icon']: true,
['el-icon-arrow-down']: true,
[$style.selectIcon]: true,
[$style.isReverse]: resourceDropdownVisible,
}"
/>
</template>
</n8n-input>
</div>
</template>
</DraggableTarget>
<ParameterIssues
v-if="parameterIssues && parameterIssues.length"
:issues="parameterIssues"
:class="$style['parameter-issues']"
/>
<div v-else-if="urlValue" :class="$style.openResourceLink">
<n8n-link theme="text" @click.stop="openResource(urlValue)">
<font-awesome-icon icon="external-link-alt" :title="getLinkAlt(valueToDisplay)" />
</n8n-link>
</div>
</div>
</div>
</div>
</ResourceLocatorDropdown>
</ResourceLocatorDropdown>
</OnClickOutside>
</div>
</template>
@ -176,6 +177,7 @@ import { useDebounce } from '@/composables/useDebounce';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRouter } from 'vue-router';
import { ndvEventBus } from '@/event-bus';
import { OnClickOutside } from '@vueuse/components';
interface IResourceLocatorQuery {
results: INodeListSearchItems[];
@ -191,6 +193,7 @@ export default defineComponent({
ExpressionParameterInput,
ParameterIssues,
ResourceLocatorDropdown,
OnClickOutside,
},
props: {
parameter: {

View file

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

View file

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

View file

@ -116,8 +116,10 @@ describe('WorkflowLMChatModal', () => {
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 VueTouchEvents from 'vue3-touch-events';
import { vOnClickOutside } from '@vueuse/components';
export const GlobalDirectivesPlugin: Plugin = {
install(app) {
app.use(VueTouchEvents);
app.directive('on-click-outside', vOnClickOutside);
},
};

View file

@ -105,7 +105,7 @@ describe('SettingsSourceControl', () => {
expect(saveSettingsButton).toBeDisabled();
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 userEvent.click(getByText('main'));
@ -137,7 +137,7 @@ describe('SettingsSourceControl', () => {
expect(refreshSshKeyButton).toBeVisible();
});
await userEvent.click(within(sshKeyTypeSelect).getByRole('textbox'));
await userEvent.click(within(sshKeyTypeSelect).getByRole('combobox'));
await waitFor(() => expect(getByText('RSA')).toBeVisible());
await userEvent.click(getByText('RSA'));
await userEvent.click(refreshSshKeyButton);

View file

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