mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
A non-archived task with no project, no tags, and not due today resolved to an empty location in NavigateToTaskService. Because ''.startsWith is always true, navigate() treated it as the current context and only tried to focus a task element that isn't on the /search page, so tapping the search result did nothing (most commonly an overdue Today/Inbox task). Route such tasks to the Today list (where overdue/context-less tasks are shown) and guard against an empty location so it surfaces an error snack instead of silently swallowing the navigation.
This commit is contained in:
parent
462fb616c9
commit
3399491f75
2 changed files with 122 additions and 0 deletions
|
|
@ -0,0 +1,111 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { NavigateToTaskService } from './navigate-to-task.service';
|
||||
import { TaskService } from '../../features/tasks/task.service';
|
||||
import { ProjectService } from '../../features/project/project.service';
|
||||
import { SnackService } from '../../core/snack/snack.service';
|
||||
import { DateService } from '../../core/date/date.service';
|
||||
import { LayoutService } from '../layout/layout.service';
|
||||
import { Task } from '../../features/tasks/task.model';
|
||||
import { TODAY_TAG } from '../../features/tag/tag.const';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
const TODAY_STR = '2026-07-06';
|
||||
|
||||
const createTask = (partial: Partial<Task>): Task =>
|
||||
({
|
||||
id: 'task-1',
|
||||
title: 'A task',
|
||||
projectId: undefined,
|
||||
parentId: undefined,
|
||||
tagIds: [],
|
||||
dueDay: undefined,
|
||||
dueWithTime: undefined,
|
||||
timeSpentOnDay: {},
|
||||
created: Date.parse('2020-01-01'),
|
||||
...partial,
|
||||
}) as unknown as Task;
|
||||
|
||||
describe('NavigateToTaskService', () => {
|
||||
let service: NavigateToTaskService;
|
||||
let taskService: jasmine.SpyObj<TaskService>;
|
||||
let router: jasmine.SpyObj<Router> & { url: string };
|
||||
let layoutService: jasmine.SpyObj<LayoutService>;
|
||||
|
||||
beforeEach(() => {
|
||||
taskService = jasmine.createSpyObj('TaskService', [
|
||||
'getByIdFromEverywhere',
|
||||
'getArchivedTasks',
|
||||
]);
|
||||
const projectService = jasmine.createSpyObj('ProjectService', [], {
|
||||
list$: of([]),
|
||||
});
|
||||
const snackService = jasmine.createSpyObj('SnackService', ['open']);
|
||||
const dateService = jasmine.createSpyObj('DateService', ['isToday', 'todayStr']);
|
||||
dateService.todayStr.and.returnValue(TODAY_STR);
|
||||
dateService.isToday.and.returnValue(false);
|
||||
layoutService = jasmine.createSpyObj('LayoutService', ['focusTaskInViewWhenReady']);
|
||||
|
||||
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
|
||||
routerSpy.navigate.and.resolveTo(true);
|
||||
routerSpy.url = '/search';
|
||||
router = routerSpy;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
NavigateToTaskService,
|
||||
{ provide: TaskService, useValue: taskService },
|
||||
{ provide: ProjectService, useValue: projectService },
|
||||
{ provide: SnackService, useValue: snackService },
|
||||
{ provide: DateService, useValue: dateService },
|
||||
{ provide: LayoutService, useValue: layoutService },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(NavigateToTaskService);
|
||||
});
|
||||
|
||||
it('navigates to the Today list for an overdue task with no project and no tags (#8780)', async () => {
|
||||
// Overdue = dueDay in the past → shown only in the Today "Overdue" section.
|
||||
taskService.getByIdFromEverywhere.and.resolveTo(
|
||||
createTask({ id: 't1', dueDay: '2020-01-01', projectId: undefined, tagIds: [] }),
|
||||
);
|
||||
|
||||
await service.navigate('t1');
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(
|
||||
[`/tag/${TODAY_TAG.id}/tasks`],
|
||||
jasmine.objectContaining({
|
||||
queryParams: jasmine.objectContaining({ focusItem: 't1' }),
|
||||
}),
|
||||
);
|
||||
// Must NOT be short-circuited into the "same context" focus-only path.
|
||||
expect(layoutService.focusTaskInViewWhenReady).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('navigates to the project list for a project task', async () => {
|
||||
taskService.getByIdFromEverywhere.and.resolveTo(
|
||||
createTask({ id: 't2', projectId: 'p1', tagIds: [] }),
|
||||
);
|
||||
|
||||
await service.navigate('t2');
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(
|
||||
['/project/p1/tasks'],
|
||||
jasmine.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('navigates to the tag list for a tagged task with no project', async () => {
|
||||
taskService.getByIdFromEverywhere.and.resolveTo(
|
||||
createTask({ id: 't3', projectId: undefined, tagIds: ['tag-a'] }),
|
||||
);
|
||||
|
||||
await service.navigate('t3');
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(
|
||||
['/tag/tag-a/tasks'],
|
||||
jasmine.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -33,6 +33,11 @@ export class NavigateToTaskService {
|
|||
throw new Error(`Task with id ${taskId} not found`);
|
||||
}
|
||||
const location = await this._getLocation(task, isArchiveTask);
|
||||
if (!location) {
|
||||
// Never fall through with an empty location: `''.startsWith` would make
|
||||
// the same-context check below always true and swallow the navigation.
|
||||
throw new Error(`Could not resolve a location for task ${taskId}`);
|
||||
}
|
||||
recordSearchNavDebug('navigateToTask:start', {
|
||||
taskId,
|
||||
isArchiveTask,
|
||||
|
|
@ -101,6 +106,12 @@ export class NavigateToTaskService {
|
|||
return `/project/${taskToCheck.projectId}/${tasksOrWorklog}`;
|
||||
} else if (taskToCheck.tagIds?.length > 0 && taskToCheck.tagIds[0]) {
|
||||
return `/tag/${taskToCheck.tagIds[0]}/${tasksOrWorklog}`;
|
||||
} else if (!isArchiveTask) {
|
||||
// A non-archived task with neither project nor tags only ever lives in the
|
||||
// Today list — either due today (handled above) or overdue. Route there so
|
||||
// navigation reveals it instead of resolving to '' and silently no-op'ing
|
||||
// (an empty location makes `url.startsWith(location)` always true). (#8780)
|
||||
return `/tag/${TODAY_TAG.id}/${tasksOrWorklog}`;
|
||||
} else {
|
||||
devError("Couldn't find task location");
|
||||
return '';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue