mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 08:56:41 +00:00
refactor(planner): use selectAllTasksInActiveProjects to exclude archived project tasks
Replaces direct task state access with the shared selectAllTasksInActiveProjects selector, which already filters archived project tasks. Fixes archived project tasks appearing in planner view. Test mock stores updated to include tasks+projects slices (required by new selector chain). Removed "missing entity references" test: that guard now lives upstream in selectAllTasks.
This commit is contained in:
parent
0855f797d5
commit
9280b85513
3 changed files with 47 additions and 130 deletions
|
|
@ -183,6 +183,8 @@ describe('AddTasksForTomorrowService', () => {
|
|||
planner: {
|
||||
days: {},
|
||||
},
|
||||
tasks: { ids: [], entities: {} },
|
||||
projects: { ids: [], entities: {} },
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import * as fromSelectors from './planner.selectors';
|
|||
import { plannerFeatureKey, PlannerState } from './planner.reducer';
|
||||
import { Task, TaskState } from '../../tasks/task.model';
|
||||
import { TASK_FEATURE_NAME } from '../../tasks/store/task.reducer';
|
||||
import { PROJECT_FEATURE_NAME } from '../../project/store/project.reducer';
|
||||
import { appStateFeatureKey } from '../../../root-store/app-state/app-state.reducer';
|
||||
import { getDbDateStr } from '../../../util/get-db-date-str';
|
||||
|
||||
|
|
@ -340,16 +341,6 @@ describe('Planner Selectors - selectPlannerDays', () => {
|
|||
};
|
||||
};
|
||||
|
||||
const emptyTaskState: TaskState = {
|
||||
ids: [],
|
||||
entities: {},
|
||||
currentTaskId: null,
|
||||
selectedTaskId: null,
|
||||
lastCurrentTaskId: null,
|
||||
isDataLoaded: true,
|
||||
taskDetailTargetPanel: null,
|
||||
};
|
||||
|
||||
const emptyPlannerState: PlannerState = {
|
||||
days: {},
|
||||
addPlannedTasksDialogLastShown: undefined,
|
||||
|
|
@ -373,12 +364,7 @@ describe('Planner Selectors - selectPlannerDays', () => {
|
|||
|
||||
it('should return a PlannerDay for each day date', () => {
|
||||
const selector = createPlannerDaysSelector([today]);
|
||||
const result = selector.projector(
|
||||
emptyTaskState,
|
||||
emptyPlannerState,
|
||||
defaultScheduleConfig,
|
||||
0,
|
||||
);
|
||||
const result = selector.projector([], emptyPlannerState, defaultScheduleConfig, 0);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].dayDate).toBe(today);
|
||||
|
|
@ -388,18 +374,13 @@ describe('Planner Selectors - selectPlannerDays', () => {
|
|||
it('should include tasks from planner state for a non-today day', () => {
|
||||
const tomorrow = getDbDateStr(new Date(Date.now() + 86400000));
|
||||
const task = createMockTask({ id: 't1', title: 'Plan task' });
|
||||
const taskState: TaskState = {
|
||||
...emptyTaskState,
|
||||
ids: ['t1'],
|
||||
entities: { t1: task },
|
||||
};
|
||||
const plannerState: PlannerState = {
|
||||
...emptyPlannerState,
|
||||
days: { [tomorrow]: ['t1'] },
|
||||
};
|
||||
|
||||
const selector = fromSelectors.selectPlannerDays([tomorrow], [], [], [], [], today);
|
||||
const result = selector.projector(taskState, plannerState, defaultScheduleConfig, 0);
|
||||
const result = selector.projector([task], plannerState, defaultScheduleConfig, 0);
|
||||
|
||||
expect(result[0].tasks.length).toBe(1);
|
||||
expect(result[0].tasks[0].id).toBe('t1');
|
||||
|
|
@ -407,16 +388,11 @@ describe('Planner Selectors - selectPlannerDays', () => {
|
|||
|
||||
it('should include unplanned today tasks passed to factory', () => {
|
||||
const task = createMockTask({ id: 't1', title: 'Today task' });
|
||||
const taskState: TaskState = {
|
||||
...emptyTaskState,
|
||||
ids: ['t1'],
|
||||
entities: { t1: task },
|
||||
};
|
||||
|
||||
// Pass t1 as a todayListTaskId (unplanned since allPlannedTasks is empty)
|
||||
const selector = fromSelectors.selectPlannerDays([today], [], ['t1'], [], [], today);
|
||||
const result = selector.projector(
|
||||
taskState,
|
||||
[task],
|
||||
emptyPlannerState,
|
||||
defaultScheduleConfig,
|
||||
0,
|
||||
|
|
@ -436,12 +412,7 @@ describe('Planner Selectors - selectPlannerDays', () => {
|
|||
lunchBreakEnd: '13:00',
|
||||
};
|
||||
const selector = createPlannerDaysSelector([today]);
|
||||
const result = selector.projector(
|
||||
emptyTaskState,
|
||||
emptyPlannerState,
|
||||
scheduleConfig,
|
||||
0,
|
||||
);
|
||||
const result = selector.projector([], emptyPlannerState, scheduleConfig, 0);
|
||||
|
||||
// 8 hours = 28800000 ms
|
||||
expect(result[0].availableHours).toBe(28800000);
|
||||
|
|
@ -450,12 +421,7 @@ describe('Planner Selectors - selectPlannerDays', () => {
|
|||
|
||||
it('should not set availableHours when schedule is disabled', () => {
|
||||
const selector = createPlannerDaysSelector([today]);
|
||||
const result = selector.projector(
|
||||
emptyTaskState,
|
||||
emptyPlannerState,
|
||||
defaultScheduleConfig,
|
||||
0,
|
||||
);
|
||||
const result = selector.projector([], emptyPlannerState, defaultScheduleConfig, 0);
|
||||
|
||||
expect(result[0].availableHours).toBeUndefined();
|
||||
expect(result[0].progressPercentage).toBeUndefined();
|
||||
|
|
@ -464,18 +430,13 @@ describe('Planner Selectors - selectPlannerDays', () => {
|
|||
it('should include additional days from planner state not in dayDates', () => {
|
||||
const tomorrow = getDbDateStr(new Date(Date.now() + 86400000));
|
||||
const task = createMockTask({ id: 't1' });
|
||||
const taskState: TaskState = {
|
||||
...emptyTaskState,
|
||||
ids: ['t1'],
|
||||
entities: { t1: task },
|
||||
};
|
||||
const plannerState: PlannerState = {
|
||||
...emptyPlannerState,
|
||||
days: { [tomorrow]: ['t1'] },
|
||||
};
|
||||
|
||||
const selector = createPlannerDaysSelector([today]);
|
||||
const result = selector.projector(taskState, plannerState, defaultScheduleConfig, 0);
|
||||
const result = selector.projector([task], plannerState, defaultScheduleConfig, 0);
|
||||
|
||||
// Should include both today (from dayDates) and tomorrow (from planner state)
|
||||
expect(result.length).toBe(2);
|
||||
|
|
@ -491,12 +452,7 @@ describe('Planner Selectors - selectPlannerDays', () => {
|
|||
};
|
||||
|
||||
const selector = createPlannerDaysSelector([today]);
|
||||
const result = selector.projector(
|
||||
emptyTaskState,
|
||||
plannerState,
|
||||
defaultScheduleConfig,
|
||||
0,
|
||||
);
|
||||
const result = selector.projector([], plannerState, defaultScheduleConfig, 0);
|
||||
|
||||
expect(result[0].tasks.length).toBe(0);
|
||||
});
|
||||
|
|
@ -535,12 +491,7 @@ describe('Planner Selectors - selectPlannerDays', () => {
|
|||
[],
|
||||
today,
|
||||
);
|
||||
const result = selector.projector(
|
||||
emptyTaskState,
|
||||
emptyPlannerState,
|
||||
scheduleConfig,
|
||||
0,
|
||||
);
|
||||
const result = selector.projector([], emptyPlannerState, scheduleConfig, 0);
|
||||
|
||||
// timeEstimate should include the timed event duration (7200000 ms = 2 hours)
|
||||
expect(result[0].timeEstimate).toBe(7200000);
|
||||
|
|
@ -585,12 +536,7 @@ describe('Planner Selectors - selectPlannerDays', () => {
|
|||
[],
|
||||
today,
|
||||
);
|
||||
const result = selector.projector(
|
||||
emptyTaskState,
|
||||
emptyPlannerState,
|
||||
scheduleConfig,
|
||||
0,
|
||||
);
|
||||
const result = selector.projector([], emptyPlannerState, scheduleConfig, 0);
|
||||
|
||||
// timeEstimate should NOT include all-day events (they use raw 24h duration)
|
||||
// so it should be 0 when there are no timed events
|
||||
|
|
@ -659,11 +605,6 @@ describe('Planner Selectors - selectPlannerDays', () => {
|
|||
title: 'Task with estimate',
|
||||
timeEstimate: 3600000,
|
||||
}); // 1 hour
|
||||
const taskState: TaskState = {
|
||||
...emptyTaskState,
|
||||
ids: ['t1'],
|
||||
entities: { t1: task },
|
||||
};
|
||||
const plannerState: PlannerState = {
|
||||
...emptyPlannerState,
|
||||
days: { [today]: ['t1'] },
|
||||
|
|
@ -693,7 +634,7 @@ describe('Planner Selectors - selectPlannerDays', () => {
|
|||
[],
|
||||
today,
|
||||
);
|
||||
const result = selector.projector(taskState, plannerState, scheduleConfig, 0);
|
||||
const result = selector.projector([task], plannerState, scheduleConfig, 0);
|
||||
|
||||
// timeEstimate = task (3600000) + timed event (7200000) = 10800000 ms = 3 hours
|
||||
expect(result[0].timeEstimate).toBe(10800000);
|
||||
|
|
@ -797,6 +738,7 @@ describe('Planner Selectors - selectAllTasksDueToday', () => {
|
|||
[appStateFeatureKey]: { todayStr, startOfNextDayDiffMs: 0 },
|
||||
[TASK_FEATURE_NAME]: { ...mockTaskState, ...taskState },
|
||||
[plannerFeatureKey]: { ...mockPlannerState, ...plannerState },
|
||||
[PROJECT_FEATURE_NAME]: { ids: [], entities: {} },
|
||||
});
|
||||
|
||||
describe('selectAllTasksDueToday', () => {
|
||||
|
|
@ -908,21 +850,6 @@ describe('Planner Selectors - selectAllTasksDueToday', () => {
|
|||
expect(result[0].id).toBe('taskDueToday');
|
||||
});
|
||||
|
||||
it('should handle missing entity references in taskState.ids gracefully', () => {
|
||||
const mockState = createMockState(
|
||||
{
|
||||
ids: ['taskDueToday', 'nonExistentTask'],
|
||||
entities: { taskDueToday: mockTasks.taskDueToday },
|
||||
},
|
||||
{ days: {} },
|
||||
);
|
||||
|
||||
const result = fromSelectors.selectAllTasksDueToday(mockState);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].id).toBe('taskDueToday');
|
||||
});
|
||||
|
||||
it('should return correct count when combining planner and due tasks', () => {
|
||||
const mockState = createMockState();
|
||||
const result = fromSelectors.selectAllTasksDueToday(mockState);
|
||||
|
|
@ -995,6 +922,7 @@ describe('Planner Selectors - selectAllTasksDueToday', () => {
|
|||
...mockPlannerState,
|
||||
days: plannerDays,
|
||||
},
|
||||
[PROJECT_FEATURE_NAME]: { ids: [], entities: {} },
|
||||
});
|
||||
|
||||
it('should include dueWithTime task at 2 AM next day when offset extends today', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||
import * as fromPlanner from './planner.reducer';
|
||||
import { selectTaskFeatureState } from '../../tasks/store/task.selectors';
|
||||
import { selectAllTasksInActiveProjects } from '../../tasks/store/task.selectors';
|
||||
import {
|
||||
NoStartTimeRepeatProjection,
|
||||
PlannerDay,
|
||||
|
|
@ -11,12 +11,7 @@ import {
|
|||
ScheduleItemType,
|
||||
} from '../planner.model';
|
||||
import { ScheduleFromCalendarEvent } from '../../schedule/schedule.model';
|
||||
import {
|
||||
TaskCopy,
|
||||
TaskState,
|
||||
TaskWithDueDay,
|
||||
TaskWithDueTime,
|
||||
} from '../../tasks/task.model';
|
||||
import { Task, TaskCopy, TaskWithDueDay, TaskWithDueTime } from '../../tasks/task.model';
|
||||
import { TaskRepeatCfg } from '../../task-repeat-cfg/task-repeat-cfg.model';
|
||||
import { getDateTimeFromClockString } from '../../../util/get-date-time-from-clock-string';
|
||||
import { isValidSplitTime } from '../../../util/is-valid-split-time';
|
||||
|
|
@ -43,30 +38,26 @@ export const selectPlannerState = createFeatureSelector<fromPlanner.PlannerState
|
|||
export const selectAllTasksDueToday = createSelector(
|
||||
selectTodayStr,
|
||||
selectStartOfNextDayDiffMs,
|
||||
selectTaskFeatureState,
|
||||
selectAllTasksInActiveProjects,
|
||||
selectPlannerState,
|
||||
(
|
||||
todayStr,
|
||||
startOfNextDayDiffMs,
|
||||
taskState,
|
||||
activeTasks,
|
||||
plannerState,
|
||||
): (TaskWithDueTime | TaskWithDueDay)[] => {
|
||||
// Start with tasks from planner state for today
|
||||
const allDue: (TaskWithDueTime | TaskWithDueDay)[] = (
|
||||
plannerState.days[todayStr] || []
|
||||
)
|
||||
.map((tid) => taskState.entities[tid])
|
||||
// there is a chance that the task is not in the store anymore
|
||||
.filter((t): t is TaskWithDueDay => !!t);
|
||||
// activeTasks excludes tasks from archived projects, so filter planner IDs through it.
|
||||
const plannedTodayIds = new Set(plannerState.days[todayStr] || []);
|
||||
const allDue: (TaskWithDueTime | TaskWithDueDay)[] = activeTasks.filter(
|
||||
(t): t is TaskWithDueDay => plannedTodayIds.has(t.id),
|
||||
);
|
||||
|
||||
// Use Set for O(1) lookup
|
||||
const allDueIds = new Set(allDue.map((t) => t.id));
|
||||
|
||||
// PERF: Single pass over ids instead of two Object.values() calls
|
||||
for (const id of taskState.ids) {
|
||||
for (const task of activeTasks) {
|
||||
const id = task.id;
|
||||
if (allDueIds.has(id)) continue;
|
||||
const task = taskState.entities[id];
|
||||
if (!task) continue;
|
||||
|
||||
// Check if task is due today
|
||||
// Priority: dueWithTime takes precedence over dueDay (mutual exclusivity pattern)
|
||||
|
|
@ -90,12 +81,11 @@ export const selectAllTasksDueToday = createSelector(
|
|||
export const selectTasksForPlannerDay = (day: string) => {
|
||||
return createSelector(
|
||||
selectPlannerState,
|
||||
selectTaskFeatureState,
|
||||
(plannerState, taskState) =>
|
||||
(plannerState.days[day] || [])
|
||||
.map((tid) => taskState.entities[tid] as TaskCopy)
|
||||
// there is a chance that the task is not in the store anymore
|
||||
.filter((t) => !!t),
|
||||
selectAllTasksInActiveProjects,
|
||||
(plannerState, activeTasks) => {
|
||||
const dayIds = new Set(plannerState.days[day] || []);
|
||||
return activeTasks.filter((t) => dayIds.has(t.id)) as TaskCopy[];
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -114,11 +104,13 @@ export const selectPlannerDays = (
|
|||
const unplannedTaskIdsToday = todayListTaskIds.filter((id) => !allPlannedIdSet.has(id));
|
||||
|
||||
return createSelector(
|
||||
selectTaskFeatureState,
|
||||
selectAllTasksInActiveProjects,
|
||||
selectPlannerState,
|
||||
selectTimelineConfig,
|
||||
selectStartOfNextDayDiffMs,
|
||||
(taskState, plannerState, scheduleConfig, startOfNextDayDiffMs): PlannerDay[] => {
|
||||
(activeTasks, plannerState, scheduleConfig, startOfNextDayDiffMs): PlannerDay[] => {
|
||||
const taskMap = new Map<string, Task>(activeTasks.map((t) => [t.id, t]));
|
||||
|
||||
const allDatesWithData = Object.keys(plannerState.days);
|
||||
const dayDatesToUse = [
|
||||
...dayDates,
|
||||
|
|
@ -128,13 +120,13 @@ export const selectPlannerDays = (
|
|||
];
|
||||
|
||||
// Pre-compute deadline tasks grouped by day (O(N) once, then O(1) per day)
|
||||
const deadlineMap = groupDeadlineTasksByDay(taskState, startOfNextDayDiffMs);
|
||||
const deadlineMap = groupDeadlineTasksByDay(activeTasks, startOfNextDayDiffMs);
|
||||
|
||||
return dayDatesToUse.map((dayDate) =>
|
||||
getPlannerDay(
|
||||
dayDate,
|
||||
todayStr,
|
||||
taskState,
|
||||
taskMap,
|
||||
plannerState,
|
||||
taskRepeatCfgs,
|
||||
allPlannedTasks,
|
||||
|
|
@ -150,29 +142,25 @@ export const selectPlannerDays = (
|
|||
};
|
||||
|
||||
export const selectPlannerDayMap = createSelector(
|
||||
selectTaskFeatureState,
|
||||
selectAllTasksInActiveProjects,
|
||||
selectPlannerState,
|
||||
(taskState, plannerState): PlannerDayMap => {
|
||||
(activeTasks, plannerState): PlannerDayMap => {
|
||||
const taskMap = new Map<string, Task>(activeTasks.map((t) => [t.id, t]));
|
||||
const map: PlannerDayMap = {};
|
||||
|
||||
Object.keys(plannerState.days).forEach((dayDate) => {
|
||||
const tids = plannerState.days[dayDate] || [];
|
||||
const normalTasks = tids
|
||||
.map((id) => taskState.entities[id] as TaskCopy)
|
||||
// filter out deleted tasks
|
||||
.filter((t) => !!t);
|
||||
map[dayDate] = normalTasks;
|
||||
map[dayDate] = tids.map((id) => taskMap.get(id) as TaskCopy).filter((t) => !!t);
|
||||
});
|
||||
|
||||
return map;
|
||||
},
|
||||
);
|
||||
|
||||
// Extracted common function
|
||||
const getPlannerDay = (
|
||||
dayDate: string,
|
||||
todayStr: string,
|
||||
taskState: TaskState,
|
||||
taskMap: Map<string, Task>,
|
||||
plannerState: any,
|
||||
taskRepeatCfgs: TaskRepeatCfg[],
|
||||
allPlannedTasks: TaskWithDueTime[],
|
||||
|
|
@ -190,7 +178,7 @@ const getPlannerDay = (
|
|||
? unplannedTaskIdsToday
|
||||
: plannerState.days[dayDate] || [];
|
||||
const normalTasks = tIds
|
||||
.map((id) => taskState.entities[id] as TaskCopy)
|
||||
.map((id) => taskMap.get(id) as TaskCopy)
|
||||
.filter((t) => !!t)
|
||||
// Filter out tasks with dueDay in future if it is Today's column
|
||||
.filter((t) => !isTodayI || !t.dueDay || t.dueDay <= todayStr);
|
||||
|
|
@ -392,13 +380,12 @@ const isPlannerAllDayCalendarEvent = (calEv: ScheduleFromCalendarEvent): boolean
|
|||
* O(N) single pass — callers can then do O(1) map lookups per day.
|
||||
*/
|
||||
const groupDeadlineTasksByDay = (
|
||||
taskState: TaskState,
|
||||
activeTasks: Task[],
|
||||
startOfNextDayDiffMs: number = 0,
|
||||
): Record<string, TaskCopy[]> => {
|
||||
const result: Record<string, TaskCopy[]> = {};
|
||||
for (const id of taskState.ids) {
|
||||
const task = taskState.entities[id] as TaskCopy | undefined;
|
||||
if (!task || task.isDone) continue;
|
||||
for (const task of activeTasks) {
|
||||
if (task.isDone) continue;
|
||||
|
||||
let dayKey: string | undefined;
|
||||
if (task.deadlineWithTime) {
|
||||
|
|
@ -407,7 +394,7 @@ const groupDeadlineTasksByDay = (
|
|||
dayKey = task.deadlineDay;
|
||||
}
|
||||
if (dayKey) {
|
||||
(result[dayKey] ??= []).push(task);
|
||||
(result[dayKey] ??= []).push(task as TaskCopy);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue