mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
fix(project): carry sections forward when duplicating a project (#8872)
* fix(project): carry sections forward when duplicating a project Duplicating a project recreated its tasks and notes but never touched the section store, so the copy came up with a flat, uncategorized task list. Duplicate the template's sections too, remapping each section's taskIds through an old->new task id map so tasks keep their section membership in the copy. Fixes #8293 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address PR feedback --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
18c48d5ea8
commit
ed26c2a9ff
2 changed files with 119 additions and 2 deletions
|
|
@ -19,6 +19,8 @@ import { WorkContextType } from '../work-context/work-context.model';
|
|||
import { T } from '../../t.const';
|
||||
import { selectNoteFeatureState } from '../note/store/note.reducer';
|
||||
import { NoteState } from '../note/note.model';
|
||||
import { selectSectionFeatureState } from '../section/store/section.selectors';
|
||||
import { SectionState } from '../section/section.model';
|
||||
import { DateService } from '../../core/date/date.service';
|
||||
import {
|
||||
selectUnarchivedProjects,
|
||||
|
|
@ -88,6 +90,45 @@ describe('ProjectService', () => {
|
|||
},
|
||||
todayOrder: [],
|
||||
};
|
||||
|
||||
const initialSectionState: SectionState = {
|
||||
ids: ['section-1', 'section-2', 'section-other', 'section-today'],
|
||||
entities: {
|
||||
'section-1': {
|
||||
id: 'section-1',
|
||||
contextId: 'project-1',
|
||||
contextType: WorkContextType.PROJECT,
|
||||
title: 'Section 1',
|
||||
isExpanded: true,
|
||||
taskIds: ['task-1'],
|
||||
},
|
||||
'section-2': {
|
||||
id: 'section-2',
|
||||
contextId: 'project-1',
|
||||
contextType: WorkContextType.PROJECT,
|
||||
title: 'Section 2',
|
||||
isExpanded: true,
|
||||
taskIds: ['task-2'],
|
||||
},
|
||||
// Foreign-context sections must NOT be copied when duplicating project-1
|
||||
'section-other': {
|
||||
id: 'section-other',
|
||||
contextId: 'project-2',
|
||||
contextType: WorkContextType.PROJECT,
|
||||
title: 'Other Project Section',
|
||||
isExpanded: true,
|
||||
taskIds: [],
|
||||
},
|
||||
'section-today': {
|
||||
id: 'section-today',
|
||||
contextId: 'TODAY',
|
||||
contextType: WorkContextType.TAG,
|
||||
title: 'Today Section',
|
||||
isExpanded: true,
|
||||
taskIds: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -191,6 +232,7 @@ describe('ProjectService', () => {
|
|||
store = TestBed.inject(Store) as MockStore<any>;
|
||||
store.overrideSelector(selectTaskFeatureState, initialTaskState);
|
||||
store.overrideSelector(selectNoteFeatureState, initialNoteState);
|
||||
store.overrideSelector(selectSectionFeatureState, initialSectionState);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -379,6 +421,43 @@ describe('ProjectService', () => {
|
|||
expect(addNoteCalls.length).toBe(1);
|
||||
expect((addNoteCalls[0][0] as any).note.isPinnedToToday).toBe(false);
|
||||
}));
|
||||
|
||||
it('should duplicate sections and remap their task membership', fakeAsync(() => {
|
||||
const project = createProject({
|
||||
id: 'project-1',
|
||||
title: 'Project 1',
|
||||
taskIds: ['task-1', 'task-2'],
|
||||
});
|
||||
spyOn(service, 'getByIdOnce$').and.returnValue(of(project));
|
||||
const dispatchSpy = spyOn(store, 'dispatch').and.callThrough();
|
||||
service.duplicateProject('project-1');
|
||||
tick();
|
||||
const addSectionCalls = dispatchSpy.calls
|
||||
.allArgs()
|
||||
.filter((args: any) => args[0]?.type === '[Section] Add Section');
|
||||
// Only project-1's sections — not project-2's or the TODAY section
|
||||
expect(addSectionCalls.length).toBe(2);
|
||||
const copiedTitles = addSectionCalls.map((args: any) => args[0].section.title);
|
||||
expect(copiedTitles).not.toContain('Other Project Section');
|
||||
expect(copiedTitles).not.toContain('Today Section');
|
||||
// task-1 -> new-task-1 (first parent duplicated)
|
||||
// task-2 -> new-task-3 (after task-1's subtask new-task-2)
|
||||
expect((addSectionCalls[0][0] as any).section).toEqual(
|
||||
jasmine.objectContaining({
|
||||
contextId: 'new-project-id',
|
||||
contextType: WorkContextType.PROJECT,
|
||||
title: 'Section 1',
|
||||
taskIds: ['new-task-1'],
|
||||
}),
|
||||
);
|
||||
expect((addSectionCalls[1][0] as any).section).toEqual(
|
||||
jasmine.objectContaining({
|
||||
contextId: 'new-project-id',
|
||||
title: 'Section 2',
|
||||
taskIds: ['new-task-3'],
|
||||
}),
|
||||
);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('unarchive', () => {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ import { sortByTitle } from '../../util/sort-by-title';
|
|||
import { Note } from '../note/note.model';
|
||||
import { selectNoteFeatureState } from '../note/store/note.reducer';
|
||||
import { addNote } from '../note/store/note.actions';
|
||||
import { Section } from '../section/section.model';
|
||||
import { addSection } from '../section/store/section.actions';
|
||||
import { selectSectionsByContextIdMap } from '../section/store/section.selectors';
|
||||
import { DialogConfirmComponent } from '../../ui/dialog-confirm/dialog-confirm.component';
|
||||
import { LOCAL_ACTIONS } from '../../util/local-actions.token';
|
||||
import { DateService } from '../../core/date/date.service';
|
||||
|
|
@ -502,9 +505,16 @@ export class ProjectService {
|
|||
const newNoteIds = this._duplicateNotesToProject(notesToCopy, newProjectId);
|
||||
this.update(newProjectId, { noteIds: newNoteIds });
|
||||
|
||||
this._duplicateTasksToProject(parentTasks, newProjectId, false, taskState);
|
||||
const sectionsMap = await firstValueFrom(
|
||||
this._store$.select(selectSectionsByContextIdMap),
|
||||
);
|
||||
const sectionsToCopy = sectionsMap.get(templateProjectId) ?? [];
|
||||
|
||||
this._duplicateTasksToProject(backlogTasks, newProjectId, true, taskState);
|
||||
const taskIdMap = new Map<string, string>();
|
||||
this._duplicateTasksToProject(parentTasks, newProjectId, false, taskState, taskIdMap);
|
||||
this._duplicateTasksToProject(backlogTasks, newProjectId, true, taskState, taskIdMap);
|
||||
|
||||
this._duplicateSectionsToProject(sectionsToCopy, newProjectId, taskIdMap);
|
||||
|
||||
return newProjectId;
|
||||
}
|
||||
|
|
@ -514,6 +524,7 @@ export class ProjectService {
|
|||
newProjectId: string,
|
||||
isBacklog: boolean,
|
||||
taskState: TaskState,
|
||||
taskIdMap: Map<string, string>,
|
||||
): void {
|
||||
// For each parent task create a copy in the new project and then copy its subtasks
|
||||
for (const p of tasks) {
|
||||
|
|
@ -541,6 +552,7 @@ export class ProjectService {
|
|||
workContextType: WorkContextType.PROJECT,
|
||||
workContextId: newProjectId,
|
||||
});
|
||||
taskIdMap.set(p.id, newParentTask.id);
|
||||
|
||||
// dispatch addTask for the parent task
|
||||
this._store$.dispatch(
|
||||
|
|
@ -580,6 +592,7 @@ export class ProjectService {
|
|||
workContextType: WorkContextType.PROJECT,
|
||||
workContextId: newProjectId,
|
||||
});
|
||||
taskIdMap.set(st.id, newSub.id);
|
||||
|
||||
this._store$.dispatch(addSubTask({ task: newSub, parentId: newParentTask.id }));
|
||||
}
|
||||
|
|
@ -607,4 +620,29 @@ export class ProjectService {
|
|||
}
|
||||
return newNoteIds;
|
||||
}
|
||||
|
||||
private _duplicateSectionsToProject(
|
||||
sections: Section[],
|
||||
newProjectId: string,
|
||||
taskIdMap: Map<string, string>,
|
||||
): void {
|
||||
for (const section of sections) {
|
||||
const newTaskIds = section.taskIds
|
||||
.map((id) => taskIdMap.get(id))
|
||||
.filter((id): id is string => !!id);
|
||||
|
||||
this._store$.dispatch(
|
||||
addSection({
|
||||
section: {
|
||||
id: nanoid(),
|
||||
contextId: newProjectId,
|
||||
contextType: WorkContextType.PROJECT,
|
||||
title: section.title,
|
||||
isExpanded: section.isExpanded,
|
||||
taskIds: newTaskIds,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue