mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
fix(task-view-customizer): show all tasks when grouping by project or tag
When "group by project" or "group by tag" was selected, only tasks from the current work context were shown. Switch the task source to all undone tasks from the store (excluding hidden/archived projects and backlog) when cross-context grouping is active. Closes #7050
This commit is contained in:
parent
375274e3ea
commit
de1b5d485c
2 changed files with 171 additions and 7 deletions
|
|
@ -11,6 +11,7 @@ import { getDbDateStr } from '../../util/get-db-date-str';
|
|||
import { Observable, of } from 'rxjs';
|
||||
import { WorkContextType } from '../work-context/work-context.model';
|
||||
import { WorkContextService } from '../work-context/work-context.service';
|
||||
import { selectAllTasksWithSubTasks } from '../tasks/store/task.selectors';
|
||||
import { ProjectService } from '../project/project.service';
|
||||
import { TagService } from '../tag/tag.service';
|
||||
import {
|
||||
|
|
@ -1189,4 +1190,135 @@ describe('TaskViewCustomizerService', () => {
|
|||
expect(newService.selectedFilter()).toEqual(DEFAULT_OPTIONS.filter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('customizeUndoneTasks with group by project (issue #7050)', () => {
|
||||
const inboxTask: TaskWithSubTasks = {
|
||||
id: 'inbox-task',
|
||||
title: 'Inbox Task',
|
||||
tagIds: [],
|
||||
projectId: 'INBOX_PROJECT',
|
||||
timeEstimate: 0,
|
||||
timeSpentOnDay: {},
|
||||
created: 1,
|
||||
subTasks: [],
|
||||
subTaskIds: [],
|
||||
timeSpent: 0,
|
||||
isDone: false,
|
||||
attachments: [],
|
||||
} as TaskWithSubTasks;
|
||||
|
||||
const projectATask: TaskWithSubTasks = {
|
||||
id: 'project-a-task',
|
||||
title: 'Project A Task',
|
||||
tagIds: [],
|
||||
projectId: 'Project A',
|
||||
timeEstimate: 0,
|
||||
timeSpentOnDay: {},
|
||||
created: 2,
|
||||
subTasks: [],
|
||||
subTaskIds: [],
|
||||
timeSpent: 0,
|
||||
isDone: false,
|
||||
attachments: [],
|
||||
} as TaskWithSubTasks;
|
||||
|
||||
const projectBTask: TaskWithSubTasks = {
|
||||
id: 'project-b-task',
|
||||
title: 'Project B Task',
|
||||
tagIds: [],
|
||||
projectId: 'Project B',
|
||||
timeEstimate: 0,
|
||||
timeSpentOnDay: {},
|
||||
created: 3,
|
||||
subTasks: [],
|
||||
subTaskIds: [],
|
||||
timeSpent: 0,
|
||||
isDone: false,
|
||||
attachments: [],
|
||||
} as TaskWithSubTasks;
|
||||
|
||||
const allProjects: Project[] = [
|
||||
{ id: 'INBOX_PROJECT', title: 'Inbox', backlogTaskIds: [] } as unknown as Project,
|
||||
{ id: 'Project A', title: 'Project A', backlogTaskIds: [] } as unknown as Project,
|
||||
{ id: 'Project B', title: 'Project B', backlogTaskIds: [] } as unknown as Project,
|
||||
];
|
||||
|
||||
let testService: TaskViewCustomizerService;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
|
||||
TestBed.resetTestingModule();
|
||||
const dateAdapter = jasmine.createSpyObj<DateAdapter<Date>>('DateAdapter', [], {
|
||||
getFirstDayOfWeek: () => DEFAULT_FIRST_DAY_OF_WEEK,
|
||||
});
|
||||
|
||||
mockWorkContextService = {
|
||||
activeWorkContextId: 'INBOX_PROJECT',
|
||||
activeWorkContextType: WorkContextType.PROJECT,
|
||||
mainListTasks$: of<TaskWithSubTasks[]>([inboxTask]),
|
||||
undoneTasks$: of<TaskWithSubTasks[]>([inboxTask]),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
TaskViewCustomizerService,
|
||||
{
|
||||
provide: LanguageService,
|
||||
useValue: mockLanguageService,
|
||||
},
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: { instant: (k: string) => k },
|
||||
},
|
||||
{ provide: DateAdapter, useValue: dateAdapter },
|
||||
{ provide: WorkContextService, useValue: mockWorkContextService },
|
||||
{ provide: ProjectService, useValue: { update: projectUpdateSpy } },
|
||||
{ provide: TagService, useValue: { updateTag: tagUpdateSpy } },
|
||||
provideMockStore({
|
||||
selectors: [
|
||||
{ selector: selectAllProjects, value: allProjects },
|
||||
{ selector: selectAllTags, value: mockTags },
|
||||
{
|
||||
selector: selectAllTasksWithSubTasks,
|
||||
value: [inboxTask, projectATask, projectBTask],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
testService = TestBed.inject(TaskViewCustomizerService);
|
||||
(testService as any)._allProjects = allProjects;
|
||||
(testService as any)._allTags = mockTags;
|
||||
});
|
||||
|
||||
it('should show tasks from ALL projects when group by project is selected, not just current context', (done) => {
|
||||
// Simulate being on Inbox — undoneTasks$ only has the inbox task
|
||||
const contextUndoneTasks$ = of<TaskWithSubTasks[]>([inboxTask]);
|
||||
|
||||
// Set group to project
|
||||
testService.setGroup({ type: GROUP_OPTION_TYPE.project, label: 'Project' });
|
||||
|
||||
// customizeUndoneTasks uses toObservable which requires injection context
|
||||
const result$ = TestBed.runInInjectionContext(() =>
|
||||
testService.customizeUndoneTasks(contextUndoneTasks$),
|
||||
);
|
||||
|
||||
// Use requestAnimationFrame since customizeUndoneTasks uses animationFrameScheduler
|
||||
requestAnimationFrame(() => {
|
||||
result$.subscribe((result) => {
|
||||
expect(result.grouped).toBeDefined();
|
||||
const groupKeys = Object.keys(result.grouped!);
|
||||
// Should contain tasks from ALL projects, not just Inbox
|
||||
expect(groupKeys).toContain('Inbox');
|
||||
expect(groupKeys).toContain('Project A');
|
||||
expect(groupKeys).toContain('Project B');
|
||||
expect(result.grouped!['Inbox']?.length).toBe(1);
|
||||
expect(result.grouped!['Project A']?.length).toBe(1);
|
||||
expect(result.grouped!['Project B']?.length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Injectable, signal, inject, effect } from '@angular/core';
|
||||
import { Observable, animationFrameScheduler, combineLatest } from 'rxjs';
|
||||
import { map, observeOn, take } from 'rxjs/operators';
|
||||
import { map, observeOn, switchMap, take } from 'rxjs/operators';
|
||||
import { TaskWithSubTasks } from '../tasks/task.model';
|
||||
import { selectAllProjects } from '../project/store/project.selectors';
|
||||
import { selectAllTasksWithSubTasks } from '../tasks/store/task.selectors';
|
||||
import { selectAllTags } from './../tag/store/tag.reducer';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Project } from '../project/project.model';
|
||||
|
|
@ -114,14 +115,24 @@ export class TaskViewCustomizerService {
|
|||
list: TaskWithSubTasks[];
|
||||
grouped?: Record<string, TaskWithSubTasks[]>;
|
||||
}> {
|
||||
const group$ = toObservable(this.selectedGroup);
|
||||
const tasks$ = group$.pipe(
|
||||
switchMap((group) => {
|
||||
const isCrossContext =
|
||||
group.type === GROUP_OPTION_TYPE.project ||
|
||||
group.type === GROUP_OPTION_TYPE.tag;
|
||||
return isCrossContext ? this._allUndoneTasksWithSubTasks$() : undoneTasks$;
|
||||
}),
|
||||
);
|
||||
|
||||
return combineLatest([
|
||||
undoneTasks$,
|
||||
tasks$,
|
||||
toObservable(this.selectedSort),
|
||||
toObservable(this.selectedGroup),
|
||||
group$,
|
||||
toObservable(this.selectedFilter),
|
||||
]).pipe(
|
||||
observeOn(animationFrameScheduler),
|
||||
map(([undone, sort, group, filter]) => {
|
||||
map(([tasks, sort, group, filter]) => {
|
||||
const normalizedFilterVal = filter.preset?.trim();
|
||||
const filterValueToUse = normalizedFilterVal ?? '';
|
||||
|
||||
|
|
@ -130,12 +141,12 @@ export class TaskViewCustomizerService {
|
|||
const isDefaultGroup = !group.type;
|
||||
|
||||
if (isDefaultFilter && isDefaultSort && isDefaultGroup) {
|
||||
return { list: undone };
|
||||
return { list: tasks };
|
||||
}
|
||||
|
||||
const filtered = isDefaultFilter
|
||||
? undone
|
||||
: this.applyFilter(undone, filter.type, filterValueToUse);
|
||||
? tasks
|
||||
: this.applyFilter(tasks, filter.type, filterValueToUse);
|
||||
const sorted = isDefaultSort
|
||||
? filtered
|
||||
: this.applySort(filtered, sort.type, sort.order);
|
||||
|
|
@ -148,6 +159,27 @@ export class TaskViewCustomizerService {
|
|||
);
|
||||
}
|
||||
|
||||
private _allUndoneTasksWithSubTasks$(): Observable<TaskWithSubTasks[]> {
|
||||
return this.store.select(selectAllTasksWithSubTasks).pipe(
|
||||
map((tasks) => {
|
||||
const hiddenProjectIds = new Set(
|
||||
this._allProjects
|
||||
.filter((p) => p.isHiddenFromMenu || p.isArchived)
|
||||
.map((p) => p.id),
|
||||
);
|
||||
const backlogTaskIds = new Set(
|
||||
this._allProjects.flatMap((p) => p.backlogTaskIds || []),
|
||||
);
|
||||
return tasks.filter(
|
||||
(task) =>
|
||||
!task.isDone &&
|
||||
(!task.projectId || !hiddenProjectIds.has(task.projectId)) &&
|
||||
!backlogTaskIds.has(task.id),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private applyFilter(
|
||||
tasks: TaskWithSubTasks[],
|
||||
type: FILTER_OPTION_TYPE | FILTER_COMMON | null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue