From 2e979547a428cc8c32ba67db59f1595c333b1bf5 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:36:43 +0200 Subject: [PATCH 01/22] refactor: Unify task runner env vars (#12040) --- docker/images/n8n/n8n-task-runners.json | 8 +- .../@n8n/config/src/configs/runners.config.ts | 2 +- .../src/config/base-runner-config.ts | 10 +-- .../__tests__/js-task-runner.test.ts | 6 +- .../__tests__/task-runner.test.ts | 89 +++++++++++++++++++ packages/@n8n/task-runner/src/task-runner.ts | 8 +- .../cli/src/runners/task-runner-process.ts | 12 +-- 7 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 packages/@n8n/task-runner/src/js-task-runner/__tests__/task-runner.test.ts diff --git a/docker/images/n8n/n8n-task-runners.json b/docker/images/n8n/n8n-task-runners.json index a37c59fccb..d9575997c0 100644 --- a/docker/images/n8n/n8n-task-runners.json +++ b/docker/images/n8n/n8n-task-runners.json @@ -9,12 +9,12 @@ "PATH", "GENERIC_TIMEZONE", "N8N_RUNNERS_GRANT_TOKEN", - "N8N_RUNNERS_N8N_URI", + "N8N_RUNNERS_TASK_BROKER_URI", "N8N_RUNNERS_MAX_PAYLOAD", "N8N_RUNNERS_MAX_CONCURRENCY", - "N8N_RUNNERS_SERVER_ENABLED", - "N8N_RUNNERS_SERVER_HOST", - "N8N_RUNNERS_SERVER_PORT", + "N8N_RUNNERS_HEALTH_CHECK_SERVER_ENABLED", + "N8N_RUNNERS_HEALTH_CHECK_SERVER_HOST", + "N8N_RUNNERS_HEALTH_CHECK_SERVER_PORT", "NODE_FUNCTION_ALLOW_BUILTIN", "NODE_FUNCTION_ALLOW_EXTERNAL", "NODE_OPTIONS", diff --git a/packages/@n8n/config/src/configs/runners.config.ts b/packages/@n8n/config/src/configs/runners.config.ts index d3fca6da08..06e262fe49 100644 --- a/packages/@n8n/config/src/configs/runners.config.ts +++ b/packages/@n8n/config/src/configs/runners.config.ts @@ -24,7 +24,7 @@ export class TaskRunnersConfig { authToken: string = ''; /** IP address task runners server should listen on */ - @Env('N8N_RUNNERS_SERVER_PORT') + @Env('N8N_RUNNERS_HEALTH_CHECK_SERVER_PORT') port: number = 5679; /** IP address task runners server should listen on */ diff --git a/packages/@n8n/task-runner/src/config/base-runner-config.ts b/packages/@n8n/task-runner/src/config/base-runner-config.ts index d70f7e2ee8..a1059adf4b 100644 --- a/packages/@n8n/task-runner/src/config/base-runner-config.ts +++ b/packages/@n8n/task-runner/src/config/base-runner-config.ts @@ -2,20 +2,20 @@ import { Config, Env, Nested } from '@n8n/config'; @Config class HealthcheckServerConfig { - @Env('N8N_RUNNERS_SERVER_ENABLED') + @Env('N8N_RUNNERS_HEALTH_CHECK_SERVER_ENABLED') enabled: boolean = false; - @Env('N8N_RUNNERS_SERVER_HOST') + @Env('N8N_RUNNERS_HEALTH_CHECK_SERVER_HOST') host: string = '127.0.0.1'; - @Env('N8N_RUNNERS_SERVER_PORT') + @Env('N8N_RUNNERS_HEALTH_CHECK_SERVER_PORT') port: number = 5681; } @Config export class BaseRunnerConfig { - @Env('N8N_RUNNERS_N8N_URI') - n8nUri: string = '127.0.0.1:5679'; + @Env('N8N_RUNNERS_TASK_BROKER_URI') + taskBrokerUri: string = 'http://127.0.0.1:5679'; @Env('N8N_RUNNERS_GRANT_TOKEN') grantToken: string = ''; diff --git a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts index a99e8b9f07..439de19eac 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts @@ -34,7 +34,7 @@ describe('JsTaskRunner', () => { ...defaultConfig.baseRunnerConfig, grantToken: 'grantToken', maxConcurrency: 1, - n8nUri: 'localhost', + taskBrokerUri: 'http://localhost', ...baseRunnerOpts, }, jsRunnerConfig: { @@ -311,10 +311,10 @@ describe('JsTaskRunner', () => { }); it("should not expose task runner's env variables even if no env state is received", async () => { - process.env.N8N_RUNNERS_N8N_URI = 'http://127.0.0.1:5679'; + process.env.N8N_RUNNERS_TASK_BROKER_URI = 'http://127.0.0.1:5679'; const outcome = await execTaskWithParams({ task: newTaskWithSettings({ - code: 'return { val: $env.N8N_RUNNERS_N8N_URI }', + code: 'return { val: $env.N8N_RUNNERS_TASK_BROKER_URI }', nodeMode: 'runOnceForAllItems', }), taskData: newDataRequestResponse(inputItems.map(wrapIntoJson), { diff --git a/packages/@n8n/task-runner/src/js-task-runner/__tests__/task-runner.test.ts b/packages/@n8n/task-runner/src/js-task-runner/__tests__/task-runner.test.ts new file mode 100644 index 0000000000..c633e95688 --- /dev/null +++ b/packages/@n8n/task-runner/src/js-task-runner/__tests__/task-runner.test.ts @@ -0,0 +1,89 @@ +import { WebSocket } from 'ws'; + +import { TaskRunner } from '@/task-runner'; + +class TestRunner extends TaskRunner {} + +jest.mock('ws'); + +describe('TestRunner', () => { + describe('constructor', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should correctly construct WebSocket URI with provided taskBrokerUri', () => { + const runner = new TestRunner({ + taskType: 'test-task', + maxConcurrency: 5, + idleTimeout: 60, + grantToken: 'test-token', + maxPayloadSize: 1024, + taskBrokerUri: 'http://localhost:8080', + timezone: 'America/New_York', + healthcheckServer: { + enabled: false, + host: 'localhost', + port: 8081, + }, + }); + + expect(WebSocket).toHaveBeenCalledWith( + `ws://localhost:8080/runners/_ws?id=${runner.id}`, + expect.objectContaining({ + headers: { + authorization: 'Bearer test-token', + }, + maxPayload: 1024, + }), + ); + }); + + it('should handle different taskBrokerUri formats correctly', () => { + const runner = new TestRunner({ + taskType: 'test-task', + maxConcurrency: 5, + idleTimeout: 60, + grantToken: 'test-token', + maxPayloadSize: 1024, + taskBrokerUri: 'https://example.com:3000/path', + timezone: 'America/New_York', + healthcheckServer: { + enabled: false, + host: 'localhost', + port: 8081, + }, + }); + + expect(WebSocket).toHaveBeenCalledWith( + `ws://example.com:3000/runners/_ws?id=${runner.id}`, + expect.objectContaining({ + headers: { + authorization: 'Bearer test-token', + }, + maxPayload: 1024, + }), + ); + }); + + it('should throw an error if taskBrokerUri is invalid', () => { + expect( + () => + new TestRunner({ + taskType: 'test-task', + maxConcurrency: 5, + idleTimeout: 60, + grantToken: 'test-token', + maxPayloadSize: 1024, + taskBrokerUri: 'not-a-valid-uri', + timezone: 'America/New_York', + healthcheckServer: { + enabled: false, + host: 'localhost', + port: 8081, + }, + }), + ).toThrowError(/Invalid URL/); + }); + }); +}); diff --git a/packages/@n8n/task-runner/src/task-runner.ts b/packages/@n8n/task-runner/src/task-runner.ts index 1486af280d..f0af115b5a 100644 --- a/packages/@n8n/task-runner/src/task-runner.ts +++ b/packages/@n8n/task-runner/src/task-runner.ts @@ -92,7 +92,9 @@ export abstract class TaskRunner extends EventEmitter { this.maxConcurrency = opts.maxConcurrency; this.idleTimeout = opts.idleTimeout; - const wsUrl = `ws://${opts.n8nUri}/runners/_ws?id=${this.id}`; + const { host: taskBrokerHost } = new URL(opts.taskBrokerUri); + + const wsUrl = `ws://${taskBrokerHost}/runners/_ws?id=${this.id}`; this.ws = new WebSocket(wsUrl, { headers: { authorization: `Bearer ${opts.grantToken}`, @@ -109,11 +111,11 @@ export abstract class TaskRunner extends EventEmitter { ['ECONNREFUSED', 'ENOTFOUND'].some((code) => code === error.code) ) { console.error( - `Error: Failed to connect to n8n. Please ensure n8n is reachable at: ${opts.n8nUri}`, + `Error: Failed to connect to n8n task broker. Please ensure n8n task broker is reachable at: ${taskBrokerHost}`, ); process.exit(1); } else { - console.error(`Error: Failed to connect to n8n at ${opts.n8nUri}`); + console.error(`Error: Failed to connect to n8n task broker at ${taskBrokerHost}`); console.error('Details:', event.message || 'Unknown error'); } }); diff --git a/packages/cli/src/runners/task-runner-process.ts b/packages/cli/src/runners/task-runner-process.ts index e33157a286..d989107718 100644 --- a/packages/cli/src/runners/task-runner-process.ts +++ b/packages/cli/src/runners/task-runner-process.ts @@ -95,19 +95,19 @@ export class TaskRunnerProcess extends TypedEmitter { const grantToken = await this.authService.createGrantToken(); - const n8nUri = `127.0.0.1:${this.runnerConfig.port}`; - this.process = this.startNode(grantToken, n8nUri); + const taskBrokerUri = `http://127.0.0.1:${this.runnerConfig.port}`; + this.process = this.startNode(grantToken, taskBrokerUri); forwardToLogger(this.logger, this.process, '[Task Runner]: '); this.monitorProcess(this.process); } - startNode(grantToken: string, n8nUri: string) { + startNode(grantToken: string, taskBrokerUri: string) { const startScript = require.resolve('@n8n/task-runner/start'); return spawn('node', [startScript], { - env: this.getProcessEnvVars(grantToken, n8nUri), + env: this.getProcessEnvVars(grantToken, taskBrokerUri), }); } @@ -159,10 +159,10 @@ export class TaskRunnerProcess extends TypedEmitter { } } - private getProcessEnvVars(grantToken: string, n8nUri: string) { + private getProcessEnvVars(grantToken: string, taskBrokerUri: string) { const envVars: Record = { N8N_RUNNERS_GRANT_TOKEN: grantToken, - N8N_RUNNERS_N8N_URI: n8nUri, + N8N_RUNNERS_TASK_BROKER_URI: taskBrokerUri, N8N_RUNNERS_MAX_PAYLOAD: this.runnerConfig.maxPayload.toString(), N8N_RUNNERS_MAX_CONCURRENCY: this.runnerConfig.maxConcurrency.toString(), ...this.getPassthroughEnvVars(), From 706702dff8da3c2e949e2c98dd5b34b299a1f17c Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Thu, 5 Dec 2024 12:20:12 +0100 Subject: [PATCH 02/22] fix(editor): Don't reset all Parameter Inputs when switched to read-only (#12063) --- packages/editor-ui/src/components/ParameterInputFull.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/ParameterInputFull.vue b/packages/editor-ui/src/components/ParameterInputFull.vue index 16b6c1321e..84b697f9da 100644 --- a/packages/editor-ui/src/components/ParameterInputFull.vue +++ b/packages/editor-ui/src/components/ParameterInputFull.vue @@ -194,7 +194,8 @@ function onDrop(newParamValue: string) { watch( () => props.isReadOnly, (isReadOnly) => { - if (isReadOnly) { + // Patch fix, see https://linear.app/n8n/issue/ADO-2974/resource-mapper-values-are-emptied-when-refreshing-the-columns + if (isReadOnly && props.parameter.disabledOptions !== undefined) { valueChanged({ name: props.path, value: props.parameter.default }); } }, From b1f866326574974eb2936e6b02771346e83e7137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Thu, 5 Dec 2024 14:59:03 +0100 Subject: [PATCH 03/22] fix(editor): Fix Nodeview.v2 reinitialise based on route changes (#12062) --- packages/editor-ui/src/views/NodeView.v2.vue | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue index a3a1a76e34..56ad8f5a43 100644 --- a/packages/editor-ui/src/views/NodeView.v2.vue +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -291,7 +291,7 @@ async function initializeData() { } } -async function initializeRoute() { +async function initializeRoute(force = false) { // In case the workflow got saved we do not have to run init // as only the route changed but all the needed data is already loaded if (route.params.action === 'workflowSave') { @@ -300,6 +300,7 @@ async function initializeRoute() { } const isAlreadyInitialized = + !force && initializedWorkflowId.value && [NEW_WORKFLOW_ID, workflowId.value].includes(initializedWorkflowId.value); @@ -1489,8 +1490,10 @@ function unregisterCustomActions() { watch( () => route.name, - async () => { - await initializeRoute(); + async (newRouteName, oldRouteName) => { + // it's navigating from and existing workflow to a new workflow + const force = newRouteName === VIEWS.NEW_WORKFLOW && oldRouteName === VIEWS.WORKFLOW; + await initializeRoute(force); }, ); From 956b11a560528336a74be40f722fa05bf3cca94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Fri, 6 Dec 2024 08:52:07 +0100 Subject: [PATCH 04/22] fix(editor): Universal button snags (#11974) Co-authored-by: Csaba Tuncsik --- .../NavigationDropdown.test.ts | 69 +++++++++++++++---- .../NavigationDropdown.vue | 44 +++++++++--- .../design-system/src/css/_tokens.dark.scss | 4 ++ packages/design-system/src/css/_tokens.scss | 5 ++ .../editor-ui/src/components/MainSidebar.vue | 55 ++++++++++----- .../components/Projects/ProjectHeader.test.ts | 25 ++++++- .../src/components/Projects/ProjectHeader.vue | 46 +++++++++---- .../useGlobalEntityCreation.test.ts | 25 +++++++ .../composables/useGlobalEntityCreation.ts | 23 ++++++- .../src/plugins/i18n/locales/en.json | 1 + .../src/views/CredentialsView.test.ts | 4 +- 11 files changed, 245 insertions(+), 56 deletions(-) diff --git a/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.test.ts b/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.test.ts index a7d9936e08..fe57291225 100644 --- a/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.test.ts +++ b/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.test.ts @@ -33,7 +33,7 @@ describe('N8nNavigationDropdown', () => { it('default slot should trigger first level', async () => { const { getByTestId, queryByTestId } = render(NavigationDropdown, { slots: { default: h('button', { 'data-test-id': 'test-trigger' }) }, - props: { menu: [{ id: 'aaa', title: 'aaa', route: { name: 'projects' } }] }, + props: { menu: [{ id: 'first', title: 'first', route: { name: 'projects' } }] }, global: { plugins: [router], }, @@ -51,9 +51,9 @@ describe('N8nNavigationDropdown', () => { props: { menu: [ { - id: 'aaa', - title: 'aaa', - submenu: [{ id: 'bbb', title: 'bbb', route: { name: 'projects' } }], + id: 'first', + title: 'first', + submenu: [{ id: 'nested', title: 'nested', route: { name: 'projects' } }], }, ], }, @@ -80,9 +80,9 @@ describe('N8nNavigationDropdown', () => { props: { menu: [ { - id: 'aaa', - title: 'aaa', - submenu: [{ id: 'bbb', title: 'bbb', route: { name: 'projects' }, icon: 'user' }], + id: 'first', + title: 'first', + submenu: [{ id: 'nested', title: 'nested', route: { name: 'projects' }, icon: 'user' }], }, ], }, @@ -100,9 +100,9 @@ describe('N8nNavigationDropdown', () => { props: { menu: [ { - id: 'aaa', - title: 'aaa', - submenu: [{ id: 'bbb', title: 'bbb', route: { name: 'projects' }, icon: 'user' }], + id: 'first', + title: 'first', + submenu: [{ id: 'nested', title: 'nested', route: { name: 'projects' }, icon: 'user' }], }, ], }, @@ -114,8 +114,53 @@ describe('N8nNavigationDropdown', () => { await userEvent.click(getByTestId('navigation-submenu-item')); expect(emitted('itemClick')).toStrictEqual([ - [{ active: true, index: 'bbb', indexPath: ['-1', 'aaa', 'bbb'] }], + [{ active: true, index: 'nested', indexPath: ['-1', 'first', 'nested'] }], ]); - expect(emitted('select')).toStrictEqual([['bbb']]); + expect(emitted('select')).toStrictEqual([['nested']]); + }); + + it('should open first level on click', async () => { + const { getByTestId, getByText } = render(NavigationDropdown, { + slots: { default: h('button', { 'data-test-id': 'test-trigger' }) }, + props: { + menu: [ + { + id: 'first', + title: 'first', + }, + ], + }, + }); + expect(getByText('first')).not.toBeVisible(); + await userEvent.click(getByTestId('test-trigger')); + expect(getByText('first')).toBeVisible(); + }); + + it('should toggle nested level on mouseenter / mouseleave', async () => { + const { getByTestId, getByText } = render(NavigationDropdown, { + slots: { default: h('button', { 'data-test-id': 'test-trigger' }) }, + props: { + menu: [ + { + id: 'first', + title: 'first', + submenu: [{ id: 'nested', title: 'nested' }], + }, + ], + }, + }); + expect(getByText('first')).not.toBeVisible(); + await userEvent.click(getByTestId('test-trigger')); + expect(getByText('first')).toBeVisible(); + + expect(getByText('nested')).not.toBeVisible(); + await userEvent.hover(getByTestId('navigation-submenu')); + await waitFor(() => expect(getByText('nested')).toBeVisible()); + + await userEvent.pointer([ + { target: getByTestId('navigation-submenu') }, + { target: getByTestId('test-trigger') }, + ]); + await waitFor(() => expect(getByText('nested')).not.toBeVisible()); }); }); diff --git a/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue b/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue index 1329d9a9ed..ce728a44ba 100644 --- a/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue +++ b/packages/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue @@ -29,7 +29,7 @@ defineProps<{ }>(); const menuRef = ref(null); -const menuIndex = ref('-1'); +const ROOT_MENU_INDEX = '-1'; const emit = defineEmits<{ itemClick: [item: MenuItemRegistered]; @@ -37,7 +37,18 @@ const emit = defineEmits<{ }>(); const close = () => { - menuRef.value?.close(menuIndex.value); + menuRef.value?.close(ROOT_MENU_INDEX); +}; + +const menuTrigger = ref<'click' | 'hover'>('click'); +const onOpen = (index: string) => { + if (index !== ROOT_MENU_INDEX) return; + menuTrigger.value = 'hover'; +}; + +const onClose = (index: string) => { + if (index !== ROOT_MENU_INDEX) return; + menuTrigger.value = 'click'; }; defineExpose({ @@ -50,14 +61,16 @@ defineExpose({ ref="menuRef" mode="horizontal" unique-opened - menu-trigger="click" + :menu-trigger="menuTrigger" :ellipsis="false" :class="$style.dropdown" @select="emit('select', $event)" @keyup.escape="close" + @open="onOpen" + @close="onClose" > - + {{ item.title }} + @@ -125,17 +145,25 @@ defineExpose({ } } +.nestedSubmenu { + :global(.el-menu) { + max-height: 450px; + overflow: auto; + } +} + .submenu { padding: 5px 0 !important; :global(.el-menu--horizontal .el-menu .el-menu-item), :global(.el-menu--horizontal .el-menu .el-sub-menu__title) { color: var(--color-text-dark); + background-color: var(--color-menu-background); } :global(.el-menu--horizontal .el-menu .el-menu-item:not(.is-disabled):hover), :global(.el-menu--horizontal .el-menu .el-sub-menu__title:not(.is-disabled):hover) { - background-color: var(--color-foreground-base); + background-color: var(--color-menu-hover-background); } :global(.el-popper) { diff --git a/packages/design-system/src/css/_tokens.dark.scss b/packages/design-system/src/css/_tokens.dark.scss index 72963efcf5..a3fc653550 100644 --- a/packages/design-system/src/css/_tokens.dark.scss +++ b/packages/design-system/src/css/_tokens.dark.scss @@ -462,6 +462,10 @@ --color-configurable-node-name: var(--color-text-dark); --color-secondary-link: var(--prim-color-secondary-tint-200); --color-secondary-link-hover: var(--prim-color-secondary-tint-100); + + --color-menu-background: var(--prim-gray-740); + --color-menu-hover-background: var(--prim-gray-670); + --color-menu-active-background: var(--prim-gray-670); } body[data-theme='dark'] { diff --git a/packages/design-system/src/css/_tokens.scss b/packages/design-system/src/css/_tokens.scss index 56d5142c87..87951534ee 100644 --- a/packages/design-system/src/css/_tokens.scss +++ b/packages/design-system/src/css/_tokens.scss @@ -533,6 +533,11 @@ --color-secondary-link: var(--color-secondary); --color-secondary-link-hover: var(--color-secondary-shade-1); + // Menu + --color-menu-background: var(--prim-gray-0); + --color-menu-hover-background: var(--prim-gray-120); + --color-menu-active-background: var(--prim-gray-120); + // Generated Color Shades from 50 to 950 // Not yet used in design system @each $color in ('neutral', 'success', 'warning', 'danger') { diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 0ed9eaab58..9b294a85f1 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -291,7 +291,12 @@ const checkWidthAndAdjustSidebar = async (width: number) => { } }; -const { menu, handleSelect: handleMenuSelect } = useGlobalEntityCreation(); +const { + menu, + handleSelect: handleMenuSelect, + createProjectAppendSlotName, + projectsLimitReachedMessage, +} = useGlobalEntityCreation(); onClickOutside(createBtn as Ref, () => { createBtn.value?.close(); }); @@ -311,8 +316,8 @@ onClickOutside(createBtn as Ref, () => { :class="['clickable', $style.sideMenuCollapseButton]" @click="toggleCollapse" > - - + +
n8n @@ -323,9 +328,21 @@ onClickOutside(createBtn as Ref, () => { @select="handleMenuSelect" > +
- + - + diff --git a/packages/editor-ui/src/components/Projects/ProjectHeader.test.ts b/packages/editor-ui/src/components/Projects/ProjectHeader.test.ts index 4bc8e6d43a..21a4c8c52f 100644 --- a/packages/editor-ui/src/components/Projects/ProjectHeader.test.ts +++ b/packages/editor-ui/src/components/Projects/ProjectHeader.test.ts @@ -3,11 +3,13 @@ import { within } from '@testing-library/dom'; import { createComponentRenderer } from '@/__tests__/render'; import { mockedStore } from '@/__tests__/utils'; import { createTestProject } from '@/__tests__/data/projects'; -import { useRoute } from 'vue-router'; +import * as router from 'vue-router'; +import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router'; import ProjectHeader from '@/components/Projects/ProjectHeader.vue'; import { useProjectsStore } from '@/stores/projects.store'; import type { Project } from '@/types/projects.types'; import { ProjectTypes } from '@/types/projects.types'; +import { VIEWS } from '@/constants'; vi.mock('vue-router', async () => { const actual = await vi.importActual('vue-router'); @@ -35,13 +37,13 @@ const renderComponent = createComponentRenderer(ProjectHeader, { }, }); -let route: ReturnType; +let route: ReturnType; let projectsStore: ReturnType>; describe('ProjectHeader', () => { beforeEach(() => { createTestingPinia(); - route = useRoute(); + route = router.useRoute(); projectsStore = mockedStore(useProjectsStore); projectsStore.teamProjectsLimit = -1; @@ -159,4 +161,21 @@ describe('ProjectHeader', () => { expect(within(getByTestId('resource-add')).getByRole('button', { name: label })).toBeVisible(); }); + + it('should not render creation button in setting page', async () => { + projectsStore.currentProject = createTestProject({ type: ProjectTypes.Personal }); + vi.spyOn(router, 'useRoute').mockReturnValueOnce({ + name: VIEWS.PROJECT_SETTINGS, + } as RouteLocationNormalizedLoadedGeneric); + const { queryByTestId } = renderComponent({ + global: { + stubs: { + N8nNavigationDropdown: { + template: '
', + }, + }, + }, + }); + expect(queryByTestId('resource-add')).not.toBeInTheDocument(); + }); }); diff --git a/packages/editor-ui/src/components/Projects/ProjectHeader.vue b/packages/editor-ui/src/components/Projects/ProjectHeader.vue index ba04291b92..977abe7393 100644 --- a/packages/editor-ui/src/components/Projects/ProjectHeader.vue +++ b/packages/editor-ui/src/components/Projects/ProjectHeader.vue @@ -1,7 +1,7 @@