From fd23d4da56ed35bc1fceb5503011ff899d7a4578 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Thu, 11 Jun 2026 10:52:42 +0200 Subject: [PATCH] fix(editor): Show correct project in workflow breadcrumb for projectId links (#32103) Co-authored-by: Claude Fable 5 --- .../projects/projects.store.test.ts | 117 +++++++++++++++++- .../collaboration/projects/projects.store.ts | 6 +- 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/packages/frontend/editor-ui/src/features/collaboration/projects/projects.store.test.ts b/packages/frontend/editor-ui/src/features/collaboration/projects/projects.store.test.ts index b7f938d7692..97c84fd0b26 100644 --- a/packages/frontend/editor-ui/src/features/collaboration/projects/projects.store.test.ts +++ b/packages/frontend/editor-ui/src/features/collaboration/projects/projects.store.test.ts @@ -3,14 +3,24 @@ import { reactive } from 'vue'; import { vi } from 'vitest'; import { useProjectsStore } from './projects.store'; import * as projectsApi from './projects.api'; -import type { Project, ProjectListItem } from './projects.types'; +import type { Project, ProjectListItem, ProjectType } from './projects.types'; import { ProjectTypes } from './projects.types'; import type { ProjectRole, Scope } from '@n8n/permissions'; +type MockRoute = { + params: Record; + query: Record; + path: string; +}; + // Minimal router mock to satisfy useRoute usage in the store +const { mockRoute } = vi.hoisted(() => ({ + mockRoute: { params: {}, query: {}, path: '' } as MockRoute, +})); + vi.mock('vue-router', async (importOriginal) => ({ ...(await importOriginal()), - useRoute: () => reactive({ params: {}, query: {}, path: '' }), + useRoute: () => reactive(mockRoute), })); vi.mock('./projects.api', () => ({ @@ -221,3 +231,106 @@ describe('useProjectsStore.updateProject (partial payloads)', () => { expect(store.currentProject?.relations.length).toBe(0); }); }); + +describe('useProjectsStore.setProjectNavActiveIdByWorkflowHomeProject', () => { + const route = reactive(mockRoute); + + beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + route.params = {}; + route.query = {}; + route.path = ''; + }); + + const now = new Date().toISOString(); + const makeProject = (id: string, type: ProjectType = ProjectTypes.Team): Project => ({ + id, + name: `Project ${id}`, + description: null, + icon: { type: 'icon', value: 'layers' }, + type, + createdAt: now, + updatedAt: now, + relations: [], + scopes: [] as Scope[], + }); + + const makeStore = () => { + const store = useProjectsStore(); + store.personalProject = makeProject('personal-1', ProjectTypes.Personal); + return store; + }; + + it('fetches the workflow home project when none is loaded', async () => { + const store = makeStore(); + const teamProject = makeProject('team-1'); + mockedProjectsApi.getProject.mockResolvedValue(teamProject); + + await store.setProjectNavActiveIdByWorkflowHomeProject(teamProject); + + expect(mockedProjectsApi.getProject).toHaveBeenCalledWith(expect.anything(), 'team-1'); + expect(store.currentProject?.id).toBe('team-1'); + expect(store.projectNavActiveId).toBe('team-1'); + }); + + it('fetches the workflow home project even when the route has a projectId query param', async () => { + route.query = { projectId: 'team-1' }; + const store = makeStore(); + const teamProject = makeProject('team-1'); + mockedProjectsApi.getProject.mockResolvedValue(teamProject); + + await store.setProjectNavActiveIdByWorkflowHomeProject(teamProject); + + expect(mockedProjectsApi.getProject).toHaveBeenCalledWith(expect.anything(), 'team-1'); + expect(store.currentProject?.id).toBe('team-1'); + }); + + it('replaces a previously loaded project that does not match the workflow home project', async () => { + const store = makeStore(); + store.currentProject = makeProject('team-2'); + const teamProject = makeProject('team-1'); + mockedProjectsApi.getProject.mockResolvedValue(teamProject); + + await store.setProjectNavActiveIdByWorkflowHomeProject(teamProject); + + expect(mockedProjectsApi.getProject).toHaveBeenCalledWith(expect.anything(), 'team-1'); + expect(store.currentProject?.id).toBe('team-1'); + }); + + it('does not refetch when the workflow home project is already loaded', async () => { + const store = makeStore(); + store.currentProject = makeProject('team-1'); + + await store.setProjectNavActiveIdByWorkflowHomeProject(makeProject('team-1')); + + expect(mockedProjectsApi.getProject).not.toHaveBeenCalled(); + expect(store.currentProject?.id).toBe('team-1'); + }); + + it('sets the personal project as current for own personal workflows', async () => { + route.query = { projectId: 'personal-1' }; + const store = makeStore(); + + await store.setProjectNavActiveIdByWorkflowHomeProject( + makeProject('personal-1', ProjectTypes.Personal), + ); + + expect(mockedProjectsApi.getProject).not.toHaveBeenCalled(); + expect(store.currentProject?.id).toBe('personal-1'); + expect(store.projectNavActiveId).toBe('personal-1'); + }); + + it('marks workflows shared with the user as shared context', async () => { + const store = makeStore(); + + await store.setProjectNavActiveIdByWorkflowHomeProject( + makeProject('other-personal', ProjectTypes.Personal), + [makeProject('personal-1', ProjectTypes.Personal)], + ); + + expect(mockedProjectsApi.getProject).not.toHaveBeenCalled(); + expect(store.currentProject).toBeNull(); + expect(store.projectNavActiveId).toBe('shared'); + }); +}); diff --git a/packages/frontend/editor-ui/src/features/collaboration/projects/projects.store.ts b/packages/frontend/editor-ui/src/features/collaboration/projects/projects.store.ts index 68d1eb6f39b..ed2857deeda 100644 --- a/packages/frontend/editor-ui/src/features/collaboration/projects/projects.store.ts +++ b/packages/frontend/editor-ui/src/features/collaboration/projects/projects.store.ts @@ -254,8 +254,10 @@ export const useProjectsStore = defineStore(STORES.PROJECTS, () => { // Handle team projects projectNavActiveId.value = workflowHomeProject?.id ?? null; - if (workflowHomeProject?.id && !currentProjectId.value) { - await getProject(workflowHomeProject?.id); + // Compare against the loaded project, not currentProjectId: a `?projectId=` query param + // makes currentProjectId truthy without currentProject ever being fetched + if (workflowHomeProject?.id && currentProject.value?.id !== workflowHomeProject.id) { + await getProject(workflowHomeProject.id); } };