Fix recurring tasks duplication in productivity assistant (#313)

* Fix an issue with recurring tasks in prod assistant

* fixup! Fix an issue with recurring tasks in prod assistant
This commit is contained in:
Chris 2025-09-10 12:52:06 +03:00 committed by GitHub
parent 86fe74fdf8
commit 6dcb794a26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 122 additions and 7 deletions

View file

@ -192,9 +192,11 @@ async function serializeTask(task, userTimezone = 'UTC', options = {}) {
// For recurring task templates, show recurrence type instead of original name
// unless skipDisplayNameTransform option is true
// Skip this transformation for 'today' type queries to show actual task names
let displayName = taskJson.name;
if (
!options.skipDisplayNameTransform &&
!options.preserveOriginalName &&
taskJson.recurrence_type &&
taskJson.recurrence_type !== 'none' &&
!taskJson.recurring_parent_id
@ -516,6 +518,8 @@ async function filterTasksByParams(params, userId, userTimezone) {
// Filter by type
switch (params.type) {
case 'today':
// Ensure we exclude recurring task instances for today view
whereClause.recurring_parent_id = null;
whereClause.status = {
[Op.notIn]: [
Task.STATUS.DONE,
@ -1162,10 +1166,18 @@ router.get('/tasks', async (req, res) => {
req.currentUser.timezone
);
// Preserve original names for recurring tasks in 'today' view for productivity assistant
const serializationOptions =
req.query.type === 'today' ? { preserveOriginalName: true } : {};
const response = {
tasks: await Promise.all(
tasks.map((task) =>
serializeTask(task, req.currentUser.timezone)
serializeTask(
task,
req.currentUser.timezone,
serializationOptions
)
)
),
metrics: {
@ -1174,29 +1186,46 @@ router.get('/tasks', async (req, res) => {
tasks_in_progress_count: metrics.tasks_in_progress_count,
tasks_in_progress: await Promise.all(
metrics.tasks_in_progress.map((task) =>
serializeTask(task, req.currentUser.timezone)
serializeTask(
task,
req.currentUser.timezone,
serializationOptions
)
)
),
tasks_due_today: await Promise.all(
metrics.tasks_due_today.map((task) =>
serializeTask(task, req.currentUser.timezone)
serializeTask(
task,
req.currentUser.timezone,
serializationOptions
)
)
),
today_plan_tasks: await Promise.all(
metrics.today_plan_tasks.map((task) =>
serializeTask(task, req.currentUser.timezone)
serializeTask(
task,
req.currentUser.timezone,
serializationOptions
)
)
),
suggested_tasks: await Promise.all(
metrics.suggested_tasks.map((task) =>
serializeTask(task, req.currentUser.timezone)
serializeTask(
task,
req.currentUser.timezone,
serializationOptions
)
)
),
tasks_completed_today: await Promise.all(
metrics.tasks_completed_today.map(async (task) => {
const serialized = await serializeTask(
task,
req.currentUser.timezone
req.currentUser.timezone,
serializationOptions
);
return {
...serialized,

View file

@ -124,7 +124,7 @@ describe('Global Recurring Task Instance Filtering', () => {
expect(response.body.tasks).toBeDefined();
const taskNames = response.body.tasks.map((t) => t.name);
expect(taskNames).toContain('Daily');
expect(taskNames).toContain('Daily Workout Template'); // Now preserves original name for type=today
expect(taskNames).toContain('Regular Task');
expect(taskNames).not.toContain('Daily Workout - Aug 23');
expect(taskNames).not.toContain('Daily Workout - Aug 24');

View file

@ -605,4 +605,90 @@ describe('Recurring Tasks API', () => {
expect(parentStillExists).not.toBeNull();
});
});
describe('GET /api/tasks?type=today - Today view filtering and naming', () => {
let parentTask, childTask, regularTask;
beforeEach(async () => {
// Create a regular non-recurring task
regularTask = await Task.create({
name: 'Regular Task',
recurrence_type: 'none',
user_id: user.id,
status: 0,
today: true,
});
// Create a recurring parent task (template)
parentTask = await Task.create({
name: 'Take vitamins',
recurrence_type: 'daily',
recurrence_interval: 1,
user_id: user.id,
status: 0,
today: true,
});
// Create a recurring child task (instance)
childTask = await Task.create({
name: 'Take vitamins',
recurrence_type: 'none',
recurring_parent_id: parentTask.id,
user_id: user.id,
status: 0,
today: true,
due_date: new Date(),
});
});
it('should exclude recurring task instances from type=today API response', async () => {
const response = await agent.get('/api/tasks?type=today');
expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined();
// Should only return regular task + recurring template, not the instance
expect(response.body.tasks.length).toBe(2);
const taskIds = response.body.tasks.map((t) => t.id);
expect(taskIds).toContain(regularTask.id);
expect(taskIds).toContain(parentTask.id);
expect(taskIds).not.toContain(childTask.id); // Instance should be filtered out
});
it('should preserve original names for recurring tasks in type=today API response', async () => {
const response = await agent.get('/api/tasks?type=today');
expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined();
// Find the recurring task in the response
const recurringTask = response.body.tasks.find(
(t) => t.id === parentTask.id
);
expect(recurringTask).toBeDefined();
// Should show original name, not "Daily"
expect(recurringTask.name).toBe('Take vitamins');
expect(recurringTask.original_name).toBe('Take vitamins');
expect(recurringTask.name).not.toBe('Daily');
});
it('should show generic names for non-today API calls (backward compatibility)', async () => {
const response = await agent.get('/api/tasks'); // No type=today
expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined();
// Find the recurring task in the response
const recurringTask = response.body.tasks.find(
(t) => t.id === parentTask.id
);
expect(recurringTask).toBeDefined();
// Should show generic recurrence name for backward compatibility
expect(recurringTask.name).toBe('Daily');
expect(recurringTask.original_name).toBe('Take vitamins');
});
});
});