-
+
- {{ $locale.baseText('nodeCreator.noResults.requestTheNode') }}{{ i18n.baseText('nodeCreator.noResults.requestTheNode') }}
diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue
index 6cb414515d990..88bc766b54977 100644
--- a/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue
+++ b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue
@@ -176,9 +176,7 @@ function onBackButton() {
v-if="activeViewStack.hasSearch"
:class="$style.searchBar"
:placeholder="
- searchPlaceholder
- ? searchPlaceholder
- : $locale.baseText('nodeCreator.searchBar.searchNodes')
+ searchPlaceholder ? searchPlaceholder : i18n.baseText('nodeCreator.searchBar.searchNodes')
"
:model-value="activeViewStack.search"
@update:model-value="onSearch"
diff --git a/packages/editor-ui/src/components/NodeCredentials.vue b/packages/editor-ui/src/components/NodeCredentials.vue
index a718b91757768..1b150d0654d41 100644
--- a/packages/editor-ui/src/components/NodeCredentials.vue
+++ b/packages/editor-ui/src/components/NodeCredentials.vue
@@ -75,7 +75,6 @@ const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const nodeHelpers = useNodeHelpers();
-
const toast = useToast();
const subscribedToCredentialType = ref('');
@@ -558,7 +557,7 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s
@@ -574,7 +573,7 @@ function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): s
diff --git a/packages/editor-ui/src/components/NodeDetailsView.vue b/packages/editor-ui/src/components/NodeDetailsView.vue
index bf17d59b5f4e6..212fc4c91ee73 100644
--- a/packages/editor-ui/src/components/NodeDetailsView.vue
+++ b/packages/editor-ui/src/components/NodeDetailsView.vue
@@ -703,13 +703,13 @@ onBeforeUnmount(() => {
>
- {{ $locale.baseText('ndv.backToCanvas.waitingForTriggerWarning') }}
+ {{ i18n.baseText('ndv.backToCanvas.waitingForTriggerWarning') }}
- {{ $locale.baseText('ndv.backToCanvas') }}
+ {{ i18n.baseText('ndv.backToCanvas') }}
@@ -816,7 +816,7 @@ onBeforeUnmount(() => {
@click="onFeatureRequestClick"
>
- {{ $locale.baseText('ndv.featureRequest') }}
+ {{ i18n.baseText('ndv.featureRequest') }}
diff --git a/packages/editor-ui/src/components/NodeExecuteButton.vue b/packages/editor-ui/src/components/NodeExecuteButton.vue
index 43b4dfa7dcfd4..f801f0701c45e 100644
--- a/packages/editor-ui/src/components/NodeExecuteButton.vue
+++ b/packages/editor-ui/src/components/NodeExecuteButton.vue
@@ -65,7 +65,7 @@ const lastPopupCountUpdate = ref(0);
const codeGenerationInProgress = ref(false);
const router = useRouter();
-const { runWorkflow, runWorkflowResolvePending, stopCurrentExecution } = useRunWorkflow({ router });
+const { runWorkflow, stopCurrentExecution } = useRunWorkflow({ router });
const workflowsStore = useWorkflowsStore();
const externalHooks = useExternalHooks();
@@ -353,17 +353,10 @@ async function onClick() {
telemetry.track('User clicked execute node button', telemetryPayload);
await externalHooks.run('nodeExecuteButton.onClick', telemetryPayload);
- if (workflowsStore.isWaitingExecution) {
- await runWorkflowResolvePending({
- destinationNode: props.nodeName,
- source: 'RunData.ExecuteNodeButton',
- });
- } else {
- await runWorkflow({
- destinationNode: props.nodeName,
- source: 'RunData.ExecuteNodeButton',
- });
- }
+ await runWorkflow({
+ destinationNode: props.nodeName,
+ source: 'RunData.ExecuteNodeButton',
+ });
emit('execute');
}
diff --git a/packages/editor-ui/src/components/NodeExecutionErrorMessage.test.ts b/packages/editor-ui/src/components/NodeExecutionErrorMessage.test.ts
new file mode 100644
index 0000000000000..3bc51a18e9a34
--- /dev/null
+++ b/packages/editor-ui/src/components/NodeExecutionErrorMessage.test.ts
@@ -0,0 +1,91 @@
+import { describe, it, expect } from 'vitest';
+import NodeExecutionErrorMessage from '@/components/NodeExecutionErrorMessage.vue';
+import { createComponentRenderer } from '@/__tests__/render';
+
+const renderComponent = createComponentRenderer(NodeExecutionErrorMessage);
+
+describe('NodeExecutionErrorMessage', () => {
+ it('renders the component', () => {
+ const { getByTestId } = renderComponent({
+ props: {
+ nodeName: 'Test Node',
+ errorMessage: 'An error occurred',
+ },
+ });
+ expect(getByTestId('sanitized-error-message')).toHaveTextContent('An error occurred');
+ });
+
+ it('renders sanitized HTML in error message', () => {
+ const { getByTestId } = renderComponent({
+ props: {
+ nodeName: 'Test Node',
+ errorMessage:
+ 'Insufficient quota detected.
Learn more',
+ },
+ });
+ expect(getByTestId('sanitized-error-message')).toContainHTML(
+ 'Insufficient quota detected.
Learn more',
+ );
+ });
+
+ it('renders the link with the correct text', () => {
+ const { getByText } = renderComponent({
+ props: {
+ nodeName: 'Test Node',
+ errorMessage: 'An error occurred',
+ },
+ });
+ expect(getByText('Open node')).toBeTruthy();
+ });
+
+ it('renders the link with the correct data attributes', () => {
+ const { getByText } = renderComponent({
+ props: {
+ nodeName: 'Test Node',
+ errorMessage: 'An error occurred',
+ },
+ });
+ const link = getByText('Open node');
+ expect(link.getAttribute('data-action')).toBe('openNodeDetail');
+ expect(link.getAttribute('data-action-parameter-node')).toBe('Test Node');
+ });
+
+ it('does not render error message when it is not provided', () => {
+ const { queryByText } = renderComponent({
+ props: {
+ nodeName: 'Test Node',
+ },
+ });
+ expect(queryByText('An error occurred')).not.toBeInTheDocument();
+ });
+
+ it('sanitizes malicious script in error message', () => {
+ const { getByTestId } = renderComponent({
+ props: {
+ nodeName: 'Test Node',
+ errorMessage: '
',
+ },
+ });
+ expect(getByTestId('sanitized-error-message')).toContainHTML('
');
+ });
+
+ it('sanitizes malicious script in error message with nested tags', () => {
+ const { getByTestId } = renderComponent({
+ props: {
+ nodeName: 'Test Node',
+ errorMessage: '
',
+ },
+ });
+ expect(getByTestId('sanitized-error-message')).toContainHTML('
');
+ });
+
+ it('sanitizes malicious script in error message with script tag', () => {
+ const { container } = renderComponent({
+ props: {
+ nodeName: 'Test Node',
+ errorMessage: '',
+ },
+ });
+ expect(container.querySelector('script')).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/editor-ui/src/components/NodeExecutionErrorMessage.vue b/packages/editor-ui/src/components/NodeExecutionErrorMessage.vue
index 5786495df4eb3..26db3153ec7e2 100644
--- a/packages/editor-ui/src/components/NodeExecutionErrorMessage.vue
+++ b/packages/editor-ui/src/components/NodeExecutionErrorMessage.vue
@@ -1,5 +1,6 @@
-
+
diff --git a/packages/editor-ui/src/components/PersonalizationModal.vue b/packages/editor-ui/src/components/PersonalizationModal.vue
index 53e40f1cfb85e..c9faca5c79d00 100644
--- a/packages/editor-ui/src/components/PersonalizationModal.vue
+++ b/packages/editor-ui/src/components/PersonalizationModal.vue
@@ -605,8 +605,8 @@ const onSubmit = async (values: IPersonalizationLatestVersion) => {
{
diff --git a/packages/editor-ui/src/components/Projects/ProjectHeader.test.ts b/packages/editor-ui/src/components/Projects/ProjectHeader.test.ts
index 01c96a8ad8859..4bc8e6d43a3e1 100644
--- a/packages/editor-ui/src/components/Projects/ProjectHeader.test.ts
+++ b/packages/editor-ui/src/components/Projects/ProjectHeader.test.ts
@@ -1,4 +1,5 @@
import { createTestingPinia } from '@pinia/testing';
+import { within } from '@testing-library/dom';
import { createComponentRenderer } from '@/__tests__/render';
import { mockedStore } from '@/__tests__/utils';
import { createTestProject } from '@/__tests__/data/projects';
@@ -29,6 +30,7 @@ const renderComponent = createComponentRenderer(ProjectHeader, {
global: {
stubs: {
ProjectTabs: projectTabsSpy,
+ N8nNavigationDropdown: true,
},
},
});
@@ -41,6 +43,8 @@ describe('ProjectHeader', () => {
createTestingPinia();
route = useRoute();
projectsStore = mockedStore(useProjectsStore);
+
+ projectsStore.teamProjectsLimit = -1;
});
afterEach(() => {
@@ -62,19 +66,37 @@ describe('ProjectHeader', () => {
expect(container.querySelector('.fa-layer-group')).toBeVisible();
});
- it('should render the correct title', async () => {
- const { getByText, rerender } = renderComponent();
+ it('should render the correct title and subtitle', async () => {
+ const { getByText, queryByText, rerender } = renderComponent();
+ const subtitle = 'All the workflows, credentials and executions you have access to';
expect(getByText('Overview')).toBeVisible();
+ expect(getByText(subtitle)).toBeVisible();
projectsStore.currentProject = { type: ProjectTypes.Personal } as Project;
await rerender({});
expect(getByText('Personal')).toBeVisible();
+ expect(queryByText(subtitle)).not.toBeInTheDocument();
const projectName = 'My Project';
projectsStore.currentProject = { name: projectName } as Project;
await rerender({});
expect(getByText(projectName)).toBeVisible();
+ expect(queryByText(subtitle)).not.toBeInTheDocument();
+ });
+
+ it('should overwrite default subtitle with slot', () => {
+ const defaultSubtitle = 'All the workflows, credentials and executions you have access to';
+ const subtitle = 'Custom subtitle';
+
+ const { getByText, queryByText } = renderComponent({
+ slots: {
+ subtitle,
+ },
+ });
+
+ expect(getByText(subtitle)).toBeVisible();
+ expect(queryByText(defaultSubtitle)).not.toBeInTheDocument();
});
it('should render ProjectTabs Settings if project is team project and user has update scope', () => {
@@ -105,9 +127,10 @@ describe('ProjectHeader', () => {
it('should render ProjectTabs without Settings if project is not team project', () => {
route.params.projectId = '123';
- projectsStore.currentProject = createTestProject(
- createTestProject({ type: ProjectTypes.Personal, scopes: ['project:update'] }),
- );
+ projectsStore.currentProject = createTestProject({
+ type: ProjectTypes.Personal,
+ scopes: ['project:update'],
+ });
renderComponent();
expect(projectTabsSpy).toHaveBeenCalledWith(
@@ -117,4 +140,23 @@ describe('ProjectHeader', () => {
null,
);
});
+
+ test.each([
+ [null, 'Create'],
+ [createTestProject({ type: ProjectTypes.Personal }), 'Create in personal'],
+ [createTestProject({ type: ProjectTypes.Team }), 'Create in project'],
+ ])('in project %s should render correct create button label %s', (project, label) => {
+ projectsStore.currentProject = project;
+ const { getByTestId } = renderComponent({
+ global: {
+ stubs: {
+ N8nNavigationDropdown: {
+ template: '
',
+ },
+ },
+ },
+ });
+
+ expect(within(getByTestId('resource-add')).getByRole('button', { name: label })).toBeVisible();
+ });
});
diff --git a/packages/editor-ui/src/components/Projects/ProjectHeader.vue b/packages/editor-ui/src/components/Projects/ProjectHeader.vue
index 6432bfcfcb0f2..ba04291b929fc 100644
--- a/packages/editor-ui/src/components/Projects/ProjectHeader.vue
+++ b/packages/editor-ui/src/components/Projects/ProjectHeader.vue
@@ -1,16 +1,21 @@
@@ -51,15 +74,26 @@ const showSettings = computed(
{{ projectName }}
-
-
+
+
+ {{
+ i18n.baseText('projects.header.subtitle')
+ }}
+
-
-
-
-
+
@@ -79,6 +113,9 @@ const showSettings = computed(
}
.actions {
- margin-left: auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ padding: var(--spacing-2xs) 0 var(--spacing-l);
}
diff --git a/packages/editor-ui/src/components/Projects/ProjectNavigation.test.ts b/packages/editor-ui/src/components/Projects/ProjectNavigation.test.ts
index 6b20301d66bc2..c75e9a174ab3e 100644
--- a/packages/editor-ui/src/components/Projects/ProjectNavigation.test.ts
+++ b/packages/editor-ui/src/components/Projects/ProjectNavigation.test.ts
@@ -1,15 +1,10 @@
import { createComponentRenderer } from '@/__tests__/render';
import { createTestingPinia } from '@pinia/testing';
-import userEvent from '@testing-library/user-event';
-import { createRouter, createMemoryHistory, useRouter } from 'vue-router';
+import { createRouter, createMemoryHistory } from 'vue-router';
import { createProjectListItem } from '@/__tests__/data/projects';
import ProjectsNavigation from '@/components/Projects//ProjectNavigation.vue';
import { useProjectsStore } from '@/stores/projects.store';
import { mockedStore } from '@/__tests__/utils';
-import type { Project } from '@/types/projects.types';
-import { VIEWS } from '@/constants';
-import { useToast } from '@/composables/useToast';
-import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
vi.mock('vue-router', async () => {
const actual = await vi.importActual('vue-router');
@@ -62,10 +57,7 @@ const renderComponent = createComponentRenderer(ProjectsNavigation, {
},
});
-let router: ReturnType