diff --git a/src/app/features/project/store/project.reducer.spec.ts b/src/app/features/project/store/project.reducer.spec.ts index c360c1581b..2268ad7f9b 100644 --- a/src/app/features/project/store/project.reducer.spec.ts +++ b/src/app/features/project/store/project.reducer.spec.ts @@ -383,6 +383,32 @@ describe('projectReducer', () => { // When target is DONE and afterTaskId is null, append to end expect((r.entities as any).P1.taskIds).toEqual(['T1', 'T2', 'B1']); }); + + it('should not duplicate the task id when re-replayed (DONE, null anchor) (#8469)', () => { + const s = fakeEntityStateFromArray([ + { + id: 'P1', + taskIds: ['T1', 'T2'], + backlogTaskIds: ['B1', 'B2'], + isEnableBacklog: true, + }, + ] as Partial[]); + + const action = moveProjectTaskToRegularList({ + taskId: 'B1', + afterTaskId: null, + workContextId: 'P1', + src: 'BACKLOG', + target: 'DONE', + }) as any; + + // Re-applying the same op on top of already-applied state (snapshot + // re-replay window, #8469) must not append the id a second time. + const once = projectReducer(s as any, action); + const twice = projectReducer(once, action); + expect((twice.entities as any).P1.taskIds).toEqual(['T1', 'T2', 'B1']); + expect((twice.entities as any).P1.backlogTaskIds).toEqual(['B2']); + }); }); describe('moveTaskInTodayList (anchor-based)', () => { diff --git a/src/app/features/project/store/project.reducer.ts b/src/app/features/project/store/project.reducer.ts index 135ed833e6..1d1035aeb0 100644 --- a/src/app/features/project/store/project.reducer.ts +++ b/src/app/features/project/store/project.reducer.ts @@ -333,10 +333,12 @@ export const projectReducer = createReducer( const filteredBacklog = backlogIdsBefore.filter(filterOutId(taskId)); // When moving to DONE section with null anchor, append to end - // Otherwise use standard anchor-based positioning + // Otherwise use standard anchor-based positioning. + // Filter the id out before appending (like moveItemAfterAnchor does) so an + // op re-replayed on top of already-applied state can't duplicate it (#8469). const newTodaysTaskIds = afterTaskId === null && target === 'DONE' - ? [...todaysTaskIdsBefore, taskId] + ? [...todaysTaskIdsBefore.filter(filterOutId(taskId)), taskId] : moveItemAfterAnchor(taskId, afterTaskId, todaysTaskIdsBefore); return projectAdapter.updateOne(