From d49e5879285607cfae2ff336474dc75ea5fca9cd Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 18 Aug 2026 11:10:02 +0200 Subject: [PATCH] fix(projects): reload projects when the api recalculated positions The api renumbers all sibling positions when they drift too close together but only returns the updated project. The store kept stale positions for the siblings, so navigating to one of them pulled in its recalculated position and made it jump to a different spot in the sidebar. --- frontend/src/stores/projects.test.ts | 53 ++++++++++++++++++++++++++++ frontend/src/stores/projects.ts | 6 ++++ 2 files changed, 59 insertions(+) diff --git a/frontend/src/stores/projects.test.ts b/frontend/src/stores/projects.test.ts index 26c2b92c2..a3ef547c2 100644 --- a/frontend/src/stores/projects.test.ts +++ b/frontend/src/stores/projects.test.ts @@ -30,6 +30,22 @@ vi.mock('@/stores/base', () => ({ }), })) +const projectServiceMock = { + update: vi.fn(), + getAll: vi.fn(), + totalPages: 1, +} + +vi.mock('@/services/project', () => ({ + default: class { + update = (project: IProject) => projectServiceMock.update(project) + getAll = (...args: unknown[]) => projectServiceMock.getAll(...args) + get totalPages() { + return projectServiceMock.totalPages + } + }, +})) + function createMockProject(overrides: Partial): IProject { return { id: 1, @@ -211,4 +227,41 @@ describe('project store', () => { expect(store.getEffectiveParentProjectId(orphanedProject, 5)).toBe(5) }) }) + + describe('updateProject', () => { + beforeEach(() => { + projectServiceMock.update.mockReset() + projectServiceMock.getAll.mockReset() + projectServiceMock.totalPages = 1 + }) + + it('should not reload all projects when the api kept the position', async () => { + const store = useProjectStore() + const project = createMockProject({id: 1, position: 100}) + store.setProject(project) + projectServiceMock.update.mockResolvedValue(createMockProject({id: 1, position: 100})) + + await store.updateProject({...project, title: 'Renamed'}) + + expect(projectServiceMock.getAll).not.toHaveBeenCalled() + }) + + it('should reload all projects when the api recalculated the position', async () => { + const store = useProjectStore() + store.setProject(createMockProject({id: 1, position: 0.0001})) + store.setProject(createMockProject({id: 2, position: 100})) + projectServiceMock.update.mockResolvedValue(createMockProject({id: 1, position: 2147483648})) + projectServiceMock.getAll.mockResolvedValue([ + createMockProject({id: 1, position: 2147483648}), + createMockProject({id: 2, position: 4294967296}), + ]) + + await store.updateProject(createMockProject({id: 1, position: 0.0001})) + + expect(projectServiceMock.getAll).toHaveBeenCalled() + // Project 2 was never updated - only the reload can fix its stale position. + expect(store.projectsArray.map(p => p.id)).toEqual([1, 2]) + expect(store.projects[2].position).toBe(4294967296) + }) + }) }) diff --git a/frontend/src/stores/projects.ts b/frontend/src/stores/projects.ts index 18a3dfad2..037fc9f79 100644 --- a/frontend/src/stores/projects.ts +++ b/frontend/src/stores/projects.ts @@ -224,6 +224,12 @@ export const useProjectStore = defineStore('project', () => { const updatedProject = await projectService.update(project) setProject(project) + // The api renumbers all sibling positions once they drift too close together but + // only returns the updated project, so the rest would keep stale positions. + if (updatedProject.position !== project.position) { + await loadAllProjects() + } + // the returned project from projectService.update is the same! // in order to not create a manipulation in pinia store we have to create a new copy return updatedProject