mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
fix(electron): support today filter in local REST API
This commit is contained in:
parent
90966bb442
commit
debdedacc0
3 changed files with 185 additions and 8 deletions
|
|
@ -256,7 +256,7 @@ All responses are JSON with a consistent envelope:
|
|||
| ------------- | ------- | ----------------------------------------------------------- |
|
||||
| `query` | string | Filter by title (case-insensitive, contains) |
|
||||
| `projectId` | string | Filter by project ID |
|
||||
| `tagId` | string | Filter by tag ID |
|
||||
| `tagId` | string | Filter by tag ID. Use `TODAY` for tasks scheduled for today |
|
||||
| `includeDone` | boolean | Include completed tasks (default: false) |
|
||||
| `source` | string | `"active"` \| `"archived"` \| `"all"` (default: `"active"`) |
|
||||
|
||||
|
|
@ -272,6 +272,9 @@ curl "http://127.0.0.1:3876/tasks?source=archived"
|
|||
# Search tasks containing "meeting"
|
||||
curl "http://127.0.0.1:3876/tasks?query=meeting"
|
||||
|
||||
# List tasks scheduled for today
|
||||
curl "http://127.0.0.1:3876/tasks?tagId=TODAY"
|
||||
|
||||
# Create a task
|
||||
curl -X POST http://127.0.0.1:3876/tasks \
|
||||
-H "Content-Type: application/json" \
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { TaskArchiveService } from '../../features/archive/task-archive.service'
|
|||
import { ProjectService } from '../../features/project/project.service';
|
||||
import { TagService } from '../../features/tag/tag.service';
|
||||
import { Task, TaskWithSubTasks, TaskArchive } from '../../features/tasks/task.model';
|
||||
import { DateService } from '../date/date.service';
|
||||
import { TODAY_TAG } from '../../features/tag/tag.const';
|
||||
import {
|
||||
LocalRestApiRequestPayload,
|
||||
LocalRestApiResponsePayload,
|
||||
|
|
@ -17,6 +19,7 @@ describe('LocalRestApiHandlerService', () => {
|
|||
let taskArchiveServiceMock: jasmine.SpyObj<TaskArchiveService>;
|
||||
let projectServiceMock: jasmine.SpyObj<ProjectService>;
|
||||
let tagServiceMock: jasmine.SpyObj<TagService>;
|
||||
let dateServiceMock: jasmine.SpyObj<DateService>;
|
||||
let requestHandler: ((payload: LocalRestApiRequestPayload) => void) | null = null;
|
||||
let responsePromiseResolve: ((response: LocalRestApiResponsePayload) => void) | null =
|
||||
null;
|
||||
|
|
@ -83,6 +86,17 @@ describe('LocalRestApiHandlerService', () => {
|
|||
return responsePromise;
|
||||
};
|
||||
|
||||
const expectTaskIds = (
|
||||
response: LocalRestApiResponsePayload,
|
||||
expectedIds: string[],
|
||||
): void => {
|
||||
expect(response.body.ok).toBe(true);
|
||||
if (!response.body.ok) {
|
||||
throw new Error(`Expected success response, got ${response.body.error.code}`);
|
||||
}
|
||||
expect((response.body.data as Task[]).map((task) => task.id)).toEqual(expectedIds);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
requestHandler = null;
|
||||
responsePromiseResolve = null;
|
||||
|
|
@ -137,6 +151,14 @@ describe('LocalRestApiHandlerService', () => {
|
|||
},
|
||||
);
|
||||
|
||||
dateServiceMock = jasmine.createSpyObj<DateService>(
|
||||
'DateService',
|
||||
['todayStr', 'getStartOfNextDayDiffMs'],
|
||||
{},
|
||||
);
|
||||
dateServiceMock.todayStr.and.returnValue('2026-05-12');
|
||||
dateServiceMock.getStartOfNextDayDiffMs.and.returnValue(0);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
LocalRestApiHandlerService,
|
||||
|
|
@ -144,6 +166,7 @@ describe('LocalRestApiHandlerService', () => {
|
|||
{ provide: TaskArchiveService, useValue: taskArchiveServiceMock },
|
||||
{ provide: ProjectService, useValue: projectServiceMock },
|
||||
{ provide: TagService, useValue: tagServiceMock },
|
||||
{ provide: DateService, useValue: dateServiceMock },
|
||||
],
|
||||
});
|
||||
|
||||
|
|
@ -200,6 +223,7 @@ describe('LocalRestApiHandlerService', () => {
|
|||
|
||||
expect(response.body.ok).toBe(true);
|
||||
expect(response.status).toBe(200);
|
||||
expectTaskIds(response, ['task-1', 'task-2']);
|
||||
});
|
||||
|
||||
it('should filter tasks by query', async () => {
|
||||
|
|
@ -213,7 +237,7 @@ describe('LocalRestApiHandlerService', () => {
|
|||
createRequest('GET', '/tasks', { query: { query: 'milk' } }),
|
||||
);
|
||||
|
||||
expect(response.body.ok).toBe(true);
|
||||
expectTaskIds(response, ['task-1']);
|
||||
});
|
||||
|
||||
it('should filter tasks by projectId', async () => {
|
||||
|
|
@ -227,7 +251,7 @@ describe('LocalRestApiHandlerService', () => {
|
|||
createRequest('GET', '/tasks', { query: { projectId: 'project-1' } }),
|
||||
);
|
||||
|
||||
expect(response.body.ok).toBe(true);
|
||||
expectTaskIds(response, ['task-1']);
|
||||
});
|
||||
|
||||
it('should filter tasks by tagId', async () => {
|
||||
|
|
@ -241,7 +265,133 @@ describe('LocalRestApiHandlerService', () => {
|
|||
createRequest('GET', '/tasks', { query: { tagId: 'tag-1' } }),
|
||||
);
|
||||
|
||||
expect(response.body.ok).toBe(true);
|
||||
expectTaskIds(response, ['task-1']);
|
||||
});
|
||||
|
||||
it('should filter tasks by the virtual TODAY tag using dueDay', async () => {
|
||||
const tasks = [
|
||||
createMockTask('task-1', { dueDay: '2026-05-12' }),
|
||||
createMockTask('task-2', { dueDay: '2026-05-13' }),
|
||||
createMockTask('task-3', { dueDay: '2026-05-11' }),
|
||||
createMockTask('task-4'),
|
||||
];
|
||||
Object.defineProperty(taskServiceMock, 'allTasks$', { get: () => of(tasks) });
|
||||
|
||||
const response = await sendRequestAndWait(
|
||||
createRequest('GET', '/tasks', { query: { tagId: TODAY_TAG.id } }),
|
||||
);
|
||||
|
||||
expectTaskIds(response, ['task-1']);
|
||||
});
|
||||
|
||||
it('should combine the virtual TODAY tag filter with projectId and query', async () => {
|
||||
const tasks = [
|
||||
createMockTask('task-1', {
|
||||
title: 'Buy milk',
|
||||
projectId: 'project-1',
|
||||
dueDay: '2026-05-12',
|
||||
}),
|
||||
createMockTask('task-2', {
|
||||
title: 'Buy bread',
|
||||
projectId: 'project-2',
|
||||
dueDay: '2026-05-12',
|
||||
}),
|
||||
createMockTask('task-3', {
|
||||
title: 'Walk dog',
|
||||
projectId: 'project-1',
|
||||
dueDay: '2026-05-12',
|
||||
}),
|
||||
];
|
||||
Object.defineProperty(taskServiceMock, 'allTasks$', { get: () => of(tasks) });
|
||||
|
||||
const response = await sendRequestAndWait(
|
||||
createRequest('GET', '/tasks', {
|
||||
query: { tagId: TODAY_TAG.id, projectId: 'project-1', query: 'milk' },
|
||||
}),
|
||||
);
|
||||
|
||||
expectTaskIds(response, ['task-1']);
|
||||
});
|
||||
|
||||
it('should include done virtual TODAY tasks when includeDone=true', async () => {
|
||||
const tasks = [
|
||||
createMockTask('task-1', { dueDay: '2026-05-12', isDone: false }),
|
||||
createMockTask('task-2', { dueDay: '2026-05-12', isDone: true }),
|
||||
];
|
||||
Object.defineProperty(taskServiceMock, 'allTasks$', { get: () => of(tasks) });
|
||||
|
||||
const response = await sendRequestAndWait(
|
||||
createRequest('GET', '/tasks', {
|
||||
query: { tagId: TODAY_TAG.id, includeDone: 'true' },
|
||||
}),
|
||||
);
|
||||
|
||||
expectTaskIds(response, ['task-1', 'task-2']);
|
||||
});
|
||||
|
||||
it('should filter virtual TODAY tasks by dueWithTime and start-of-next-day offset', async () => {
|
||||
dateServiceMock.todayStr.and.returnValue('2026-02-15');
|
||||
dateServiceMock.getStartOfNextDayDiffMs.and.returnValue(4 * 60 * 60 * 1000);
|
||||
const tasks = [
|
||||
createMockTask('task-1', {
|
||||
dueWithTime: new Date(2026, 1, 16, 2, 0).getTime(),
|
||||
}),
|
||||
createMockTask('task-2', {
|
||||
dueWithTime: new Date(2026, 1, 16, 5, 0).getTime(),
|
||||
}),
|
||||
];
|
||||
Object.defineProperty(taskServiceMock, 'allTasks$', { get: () => of(tasks) });
|
||||
|
||||
const response = await sendRequestAndWait(
|
||||
createRequest('GET', '/tasks', { query: { tagId: TODAY_TAG.id } }),
|
||||
);
|
||||
|
||||
expectTaskIds(response, ['task-1']);
|
||||
});
|
||||
|
||||
it('should let dueWithTime take priority over dueDay for the virtual TODAY tag', async () => {
|
||||
const tasks = [
|
||||
createMockTask('task-1', {
|
||||
dueDay: '2026-05-12',
|
||||
dueWithTime: new Date(2026, 4, 13, 10, 0).getTime(),
|
||||
}),
|
||||
createMockTask('task-2', {
|
||||
dueDay: '2026-05-12',
|
||||
}),
|
||||
];
|
||||
Object.defineProperty(taskServiceMock, 'allTasks$', { get: () => of(tasks) });
|
||||
|
||||
const response = await sendRequestAndWait(
|
||||
createRequest('GET', '/tasks', { query: { tagId: TODAY_TAG.id } }),
|
||||
);
|
||||
|
||||
expectTaskIds(response, ['task-2']);
|
||||
});
|
||||
|
||||
it('should not fail the virtual TODAY filter for invalid dueWithTime values', async () => {
|
||||
const tasks = [
|
||||
createMockTask('task-1', {
|
||||
dueDay: '2026-05-12',
|
||||
dueWithTime: -1,
|
||||
}),
|
||||
createMockTask('task-2', {
|
||||
dueWithTime: -1,
|
||||
}),
|
||||
createMockTask('task-3', {
|
||||
dueDay: '2026-05-12',
|
||||
dueWithTime: 8_640_000_000_000_001,
|
||||
}),
|
||||
createMockTask('task-4', {
|
||||
dueWithTime: 8_640_000_000_000_001,
|
||||
}),
|
||||
];
|
||||
Object.defineProperty(taskServiceMock, 'allTasks$', { get: () => of(tasks) });
|
||||
|
||||
const response = await sendRequestAndWait(
|
||||
createRequest('GET', '/tasks', { query: { tagId: TODAY_TAG.id } }),
|
||||
);
|
||||
|
||||
expectTaskIds(response, ['task-1', 'task-3']);
|
||||
});
|
||||
|
||||
it('should exclude done tasks by default', async () => {
|
||||
|
|
@ -253,7 +403,7 @@ describe('LocalRestApiHandlerService', () => {
|
|||
|
||||
const response = await sendRequestAndWait(createRequest('GET', '/tasks'));
|
||||
|
||||
expect(response.body.ok).toBe(true);
|
||||
expectTaskIds(response, ['task-1']);
|
||||
});
|
||||
|
||||
it('should include done tasks when includeDone=true', async () => {
|
||||
|
|
@ -267,11 +417,11 @@ describe('LocalRestApiHandlerService', () => {
|
|||
createRequest('GET', '/tasks', { query: { includeDone: 'true' } }),
|
||||
);
|
||||
|
||||
expect(response.body.ok).toBe(true);
|
||||
expectTaskIds(response, ['task-1', 'task-2']);
|
||||
});
|
||||
|
||||
it('should return archived tasks when source=archived', async () => {
|
||||
const archivedTask = createMockTask('archivedTask1', { isDone: true });
|
||||
const archivedTask = createMockTask('archivedTask1');
|
||||
(taskArchiveServiceMock as any).load.and.returnValue(
|
||||
Promise.resolve({
|
||||
ids: ['archivedTask1'],
|
||||
|
|
@ -284,6 +434,7 @@ describe('LocalRestApiHandlerService', () => {
|
|||
);
|
||||
|
||||
expect(response.body.ok).toBe(true);
|
||||
expectTaskIds(response, ['archivedTask1']);
|
||||
expect(taskArchiveServiceMock.load).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -300,6 +451,7 @@ describe('LocalRestApiHandlerService', () => {
|
|||
);
|
||||
|
||||
expect(response.body.ok).toBe(true);
|
||||
expectTaskIds(response, ['task-1']);
|
||||
expect(taskServiceMock.getAllTasksEverywhere).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import { Task, TaskWithSubTasks } from '../../features/tasks/task.model';
|
|||
import { TaskArchiveService } from '../../features/archive/task-archive.service';
|
||||
import { ProjectService } from '../../features/project/project.service';
|
||||
import { TagService } from '../../features/tag/tag.service';
|
||||
import { TODAY_TAG } from '../../features/tag/tag.const';
|
||||
import { DateService } from '../date/date.service';
|
||||
import { isTodayWithOffset } from '../../util/is-today.util';
|
||||
import {
|
||||
LocalRestApiRequestPayload,
|
||||
LocalRestApiResponsePayload,
|
||||
|
|
@ -109,6 +112,20 @@ const createSuccessResponse = (
|
|||
|
||||
type TaskSource = 'active' | 'archived' | 'all';
|
||||
|
||||
const isValidTimestamp = (value: unknown): value is number =>
|
||||
typeof value === 'number' && Number.isFinite(value) && new Date(value).getTime() > 0;
|
||||
|
||||
const isTaskInToday = (
|
||||
task: Task,
|
||||
todayStr: string,
|
||||
startOfNextDayDiffMs: number,
|
||||
): boolean => {
|
||||
if (isValidTimestamp(task.dueWithTime)) {
|
||||
return isTodayWithOffset(task.dueWithTime, todayStr, startOfNextDayDiffMs);
|
||||
}
|
||||
return task.dueDay === todayStr;
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
|
|
@ -117,6 +134,7 @@ export class LocalRestApiHandlerService {
|
|||
private readonly _taskArchiveService = inject(TaskArchiveService);
|
||||
private readonly _projectService = inject(ProjectService);
|
||||
private readonly _tagService = inject(TagService);
|
||||
private readonly _dateService = inject(DateService);
|
||||
private _isInitialized = false;
|
||||
|
||||
init(): void {
|
||||
|
|
@ -293,7 +311,11 @@ export class LocalRestApiHandlerService {
|
|||
filtered = filtered.filter((t) => t.projectId === projectId);
|
||||
}
|
||||
|
||||
if (tagId) {
|
||||
if (tagId === TODAY_TAG.id) {
|
||||
const todayStr = this._dateService.todayStr();
|
||||
const startOfNextDayDiffMs = this._dateService.getStartOfNextDayDiffMs();
|
||||
filtered = filtered.filter((t) => isTaskInToday(t, todayStr, startOfNextDayDiffMs));
|
||||
} else if (tagId) {
|
||||
filtered = filtered.filter((t) => t.tagIds.includes(tagId));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue