mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 08:56:41 +00:00
feat(task): implement tag conflict resolution for parent and sub-tasks
This commit is contained in:
parent
9bf9d82d44
commit
c19b3dbb0e
3 changed files with 385 additions and 997 deletions
|
|
@ -1064,6 +1064,252 @@ describe('taskSharedCrudMetaReducer', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('parent/sub-task tag conflict resolution', () => {
|
||||
describe('addTask action', () => {
|
||||
it('should remove parent from tag when adding sub-task to same tag', () => {
|
||||
// Setup: parent task already in tag
|
||||
const testState = createStateWithExistingTasks(
|
||||
['parent-task'],
|
||||
[],
|
||||
['parent-task'],
|
||||
);
|
||||
|
||||
// Create sub-task that will be added to the same tag
|
||||
const subTask = createMockTask({
|
||||
id: 'sub-task',
|
||||
parentId: 'parent-task',
|
||||
tagIds: ['tag1'],
|
||||
});
|
||||
|
||||
const action = TaskSharedActions.addTask({
|
||||
task: subTask,
|
||||
workContextId: 'project1',
|
||||
workContextType: WorkContextType.PROJECT,
|
||||
isAddToBottom: false,
|
||||
isAddToBacklog: false,
|
||||
});
|
||||
|
||||
metaReducer(testState, action);
|
||||
|
||||
// Parent should be removed from tag, and tag removed from parent
|
||||
expectStateUpdate(
|
||||
{
|
||||
...expectTaskUpdate('parent-task', {
|
||||
tagIds: [], // Tag should be removed from parent
|
||||
}),
|
||||
...expectTagUpdate('tag1', {
|
||||
taskIds: ['sub-task'], // Only sub-task should remain
|
||||
}),
|
||||
},
|
||||
action,
|
||||
mockReducer,
|
||||
testState,
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove sub-tasks from tag when adding parent to same tag', () => {
|
||||
// Setup: sub-tasks already in tag
|
||||
const testState = createStateWithExistingTasks(
|
||||
['sub1', 'sub2', 'other-task'],
|
||||
[],
|
||||
['sub1', 'sub2', 'other-task'],
|
||||
);
|
||||
|
||||
// Update sub-tasks to have parentId
|
||||
testState[TASK_FEATURE_NAME].entities.sub1 = createMockTask({
|
||||
id: 'sub1',
|
||||
parentId: 'parent-task',
|
||||
tagIds: ['tag1'],
|
||||
});
|
||||
testState[TASK_FEATURE_NAME].entities.sub2 = createMockTask({
|
||||
id: 'sub2',
|
||||
parentId: 'parent-task',
|
||||
tagIds: ['tag1'],
|
||||
});
|
||||
|
||||
// Create parent task with sub-tasks
|
||||
const parentTask = createMockTask({
|
||||
id: 'parent-task',
|
||||
subTaskIds: ['sub1', 'sub2'],
|
||||
tagIds: ['tag1'],
|
||||
});
|
||||
|
||||
const action = TaskSharedActions.addTask({
|
||||
task: parentTask,
|
||||
workContextId: 'project1',
|
||||
workContextType: WorkContextType.PROJECT,
|
||||
isAddToBottom: false,
|
||||
isAddToBacklog: false,
|
||||
});
|
||||
|
||||
metaReducer(testState, action);
|
||||
|
||||
// Sub-tasks should be removed from tag, and tag removed from sub-tasks
|
||||
expectStateUpdate(
|
||||
{
|
||||
...expectTaskUpdate('sub1', {
|
||||
tagIds: [], // Tag should be removed
|
||||
}),
|
||||
...expectTaskUpdate('sub2', {
|
||||
tagIds: [], // Tag should be removed
|
||||
}),
|
||||
...expectTagUpdate('tag1', {
|
||||
taskIds: ['parent-task', 'other-task'], // Only parent and other-task
|
||||
}),
|
||||
},
|
||||
action,
|
||||
mockReducer,
|
||||
testState,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTask action', () => {
|
||||
it('should remove parent from tag when adding tag to sub-task', () => {
|
||||
// Setup: parent task in tag, sub-task without tag
|
||||
const testState = createStateWithExistingTasks(
|
||||
['parent-task', 'sub-task'],
|
||||
[],
|
||||
['parent-task'],
|
||||
);
|
||||
|
||||
testState[TASK_FEATURE_NAME].entities['sub-task'] = createMockTask({
|
||||
id: 'sub-task',
|
||||
parentId: 'parent-task',
|
||||
tagIds: [],
|
||||
});
|
||||
|
||||
const action = TaskSharedActions.updateTask({
|
||||
task: {
|
||||
id: 'sub-task',
|
||||
changes: { tagIds: ['tag1'] },
|
||||
},
|
||||
});
|
||||
|
||||
metaReducer(testState, action);
|
||||
|
||||
expectStateUpdate(
|
||||
{
|
||||
...expectTaskUpdate('parent-task', {
|
||||
tagIds: [], // Tag removed from parent
|
||||
}),
|
||||
...expectTaskUpdate('sub-task', {
|
||||
tagIds: ['tag1'], // Tag added to sub-task
|
||||
}),
|
||||
...expectTagUpdate('tag1', {
|
||||
taskIds: ['sub-task'], // Only sub-task in tag
|
||||
}),
|
||||
},
|
||||
action,
|
||||
mockReducer,
|
||||
testState,
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove sub-tasks from tag when adding tag to parent', () => {
|
||||
// Setup: sub-tasks in tag, parent without tag
|
||||
const testState = createStateWithExistingTasks(
|
||||
['parent-task', 'sub1', 'sub2'],
|
||||
[],
|
||||
['sub1', 'sub2'],
|
||||
);
|
||||
|
||||
testState[TASK_FEATURE_NAME].entities['parent-task'] = createMockTask({
|
||||
id: 'parent-task',
|
||||
subTaskIds: ['sub1', 'sub2'],
|
||||
tagIds: [],
|
||||
});
|
||||
testState[TASK_FEATURE_NAME].entities.sub1 = createMockTask({
|
||||
id: 'sub1',
|
||||
parentId: 'parent-task',
|
||||
tagIds: ['tag1'],
|
||||
});
|
||||
testState[TASK_FEATURE_NAME].entities.sub2 = createMockTask({
|
||||
id: 'sub2',
|
||||
parentId: 'parent-task',
|
||||
tagIds: ['tag1'],
|
||||
});
|
||||
|
||||
const action = TaskSharedActions.updateTask({
|
||||
task: {
|
||||
id: 'parent-task',
|
||||
changes: { tagIds: ['tag1'] },
|
||||
},
|
||||
});
|
||||
|
||||
metaReducer(testState, action);
|
||||
|
||||
expectStateUpdate(
|
||||
{
|
||||
...expectTaskUpdate('parent-task', {
|
||||
tagIds: ['tag1'], // Tag added to parent
|
||||
}),
|
||||
...expectTaskUpdate('sub1', {
|
||||
tagIds: [], // Tag removed from sub-task
|
||||
}),
|
||||
...expectTaskUpdate('sub2', {
|
||||
tagIds: [], // Tag removed from sub-task
|
||||
}),
|
||||
...expectTagUpdate('tag1', {
|
||||
taskIds: ['parent-task'], // Only parent in tag
|
||||
}),
|
||||
},
|
||||
action,
|
||||
mockReducer,
|
||||
testState,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertToMainTask action', () => {
|
||||
it('should remove parent from tag when converting sub-task to main task with same tag', () => {
|
||||
// Setup: parent and sub-task exist, parent is in tag
|
||||
const testState = createStateWithExistingTasks(
|
||||
['parent-task', 'sub-task'],
|
||||
[],
|
||||
['parent-task'],
|
||||
);
|
||||
|
||||
testState[TASK_FEATURE_NAME].entities['parent-task'] = createMockTask({
|
||||
id: 'parent-task',
|
||||
subTaskIds: ['sub-task'],
|
||||
tagIds: ['tag1'],
|
||||
});
|
||||
testState[TASK_FEATURE_NAME].entities['sub-task'] = createMockTask({
|
||||
id: 'sub-task',
|
||||
parentId: 'parent-task',
|
||||
tagIds: [],
|
||||
});
|
||||
|
||||
const action = TaskSharedActions.convertToMainTask({
|
||||
task: testState[TASK_FEATURE_NAME].entities['sub-task'] as Task,
|
||||
parentTagIds: ['tag1'],
|
||||
isPlanForToday: false,
|
||||
});
|
||||
|
||||
metaReducer(testState, action);
|
||||
|
||||
expectStateUpdate(
|
||||
{
|
||||
...expectTaskUpdate('parent-task', {
|
||||
tagIds: [], // Tag removed from parent
|
||||
}),
|
||||
...expectTaskUpdate('sub-task', {
|
||||
parentId: undefined, // No longer a sub-task
|
||||
tagIds: ['tag1'], // Inherited parent's tag
|
||||
}),
|
||||
...expectTagUpdate('tag1', {
|
||||
taskIds: ['sub-task'], // Only converted task in tag
|
||||
}),
|
||||
},
|
||||
action,
|
||||
mockReducer,
|
||||
testState,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('other actions', () => {
|
||||
it('should pass through other actions to the reducer', () => {
|
||||
const action = { type: 'SOME_OTHER_ACTION' };
|
||||
|
|
|
|||
|
|
@ -33,6 +33,74 @@ import {
|
|||
updateTags,
|
||||
} from './task-shared-helpers';
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Removes parent or sub-tasks from a tag's task list to maintain the constraint
|
||||
* that parent and sub-tasks should never be in the same tag list.
|
||||
* Also removes the tag from the conflicting tasks.
|
||||
*
|
||||
* @param state - The current state
|
||||
* @param taskId - The task being added
|
||||
* @param tagId - The tag to check
|
||||
* @returns Updated state with conflicts resolved
|
||||
*/
|
||||
const removeConflictingTasksFromTag = (
|
||||
state: RootState,
|
||||
taskId: string,
|
||||
tagId: string,
|
||||
): RootState => {
|
||||
const task = state[TASK_FEATURE_NAME].entities[taskId] as Task;
|
||||
if (!task) return state;
|
||||
|
||||
const tag = getTag(state, tagId);
|
||||
const conflictingTaskIds: string[] = [];
|
||||
|
||||
// If this is a sub-task, check if parent is in the tag
|
||||
if (task.parentId && tag.taskIds.includes(task.parentId)) {
|
||||
conflictingTaskIds.push(task.parentId);
|
||||
}
|
||||
|
||||
// If this is a parent task, check if any sub-tasks are in the tag
|
||||
if (task.subTaskIds && task.subTaskIds.length > 0) {
|
||||
const subTasksInTag = task.subTaskIds.filter((subId) => tag.taskIds.includes(subId));
|
||||
conflictingTaskIds.push(...subTasksInTag);
|
||||
}
|
||||
|
||||
if (conflictingTaskIds.length === 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// Update the conflicting tasks to remove the tag
|
||||
const taskUpdates: Update<Task>[] = conflictingTaskIds.map((conflictingTaskId) => {
|
||||
const conflictingTask = state[TASK_FEATURE_NAME].entities[conflictingTaskId] as Task;
|
||||
return {
|
||||
id: conflictingTaskId,
|
||||
changes: {
|
||||
tagIds: conflictingTask.tagIds.filter((id) => id !== tagId),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Update the task state
|
||||
const updatedState = {
|
||||
...state,
|
||||
[TASK_FEATURE_NAME]: taskAdapter.updateMany(taskUpdates, state[TASK_FEATURE_NAME]),
|
||||
};
|
||||
|
||||
// Update the tag to remove conflicting tasks
|
||||
const tagUpdate: Update<Tag> = {
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: tag.taskIds.filter((id) => !conflictingTaskIds.includes(id)),
|
||||
},
|
||||
};
|
||||
|
||||
return updateTags(updatedState, [tagUpdate]);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// ACTION HANDLERS
|
||||
// =============================================================================
|
||||
|
|
@ -74,11 +142,21 @@ const handleAddTask = (
|
|||
...(task.dueDay === getWorklogStr() ? [TODAY_TAG.id] : []),
|
||||
].filter((tagId) => state[TAG_FEATURE_NAME].entities[tagId]);
|
||||
|
||||
// First, handle conflicts for all tags
|
||||
for (const tagId of tagIdsToUpdate) {
|
||||
updatedState = removeConflictingTasksFromTag(updatedState, task.id, tagId);
|
||||
}
|
||||
|
||||
// Then add the task to all its tags
|
||||
const tagUpdates = tagIdsToUpdate.map(
|
||||
(tagId): Update<Tag> => ({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: addTaskToList(getTag(state, tagId).taskIds, task.id, isAddToBottom),
|
||||
taskIds: addTaskToList(
|
||||
getTag(updatedState, tagId).taskIds,
|
||||
task.id,
|
||||
isAddToBottom,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -134,6 +212,56 @@ const handleConvertToMainTask = (
|
|||
...parentTagIds,
|
||||
...(isPlanForToday ? [TODAY_TAG.id] : []),
|
||||
].filter((tagId) => state[TAG_FEATURE_NAME].entities[tagId]);
|
||||
|
||||
// For convertToMainTask, we need to manually check if parent is in the tags
|
||||
// since the parent-child relationship was already cleaned up
|
||||
const parentTaskId = task.parentId;
|
||||
if (parentTaskId) {
|
||||
// Remove parent from all tags that the converted task will be in
|
||||
const parentTaskUpdates: Update<Task>[] = [];
|
||||
const tagUpdatesForParent: Update<Tag>[] = [];
|
||||
|
||||
for (const tagId of tagIdsToUpdate) {
|
||||
const tag = getTag(updatedState, tagId);
|
||||
if (tag.taskIds.includes(parentTaskId)) {
|
||||
// Remove tag from parent task
|
||||
const currentParent = updatedState[TASK_FEATURE_NAME].entities[
|
||||
parentTaskId
|
||||
] as Task;
|
||||
if (currentParent && currentParent.tagIds.includes(tagId)) {
|
||||
parentTaskUpdates.push({
|
||||
id: parentTaskId,
|
||||
changes: {
|
||||
tagIds: currentParent.tagIds.filter((id) => id !== tagId),
|
||||
},
|
||||
});
|
||||
}
|
||||
// Remove parent from tag
|
||||
tagUpdatesForParent.push({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: tag.taskIds.filter((id) => id !== parentTaskId),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (parentTaskUpdates.length > 0) {
|
||||
updatedState = {
|
||||
...updatedState,
|
||||
[TASK_FEATURE_NAME]: taskAdapter.updateMany(
|
||||
parentTaskUpdates,
|
||||
updatedState[TASK_FEATURE_NAME],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (tagUpdatesForParent.length > 0) {
|
||||
updatedState = updateTags(updatedState, tagUpdatesForParent);
|
||||
}
|
||||
}
|
||||
|
||||
// Then add the task to all its tags at the beginning
|
||||
const tagUpdates = tagIdsToUpdate.map(
|
||||
(tagId): Update<Tag> => ({
|
||||
id: tagId,
|
||||
|
|
@ -295,11 +423,18 @@ const handleTagUpdates = (
|
|||
.filter((newId) => !oldTagIds.includes(newId))
|
||||
.filter((tagId) => state[TAG_FEATURE_NAME].entities[tagId]); // Only existing tags
|
||||
|
||||
let updatedState = state;
|
||||
|
||||
// First, handle conflicts for all tags we're adding to
|
||||
for (const tagId of tagsToAddTo) {
|
||||
updatedState = removeConflictingTasksFromTag(updatedState, taskId, tagId);
|
||||
}
|
||||
|
||||
const removeUpdates = tagsToRemoveFrom.map(
|
||||
(tagId): Update<Tag> => ({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: getTag(state, tagId).taskIds.filter((id) => id !== taskId),
|
||||
taskIds: getTag(updatedState, tagId).taskIds.filter((id) => id !== taskId),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -308,12 +443,12 @@ const handleTagUpdates = (
|
|||
(tagId): Update<Tag> => ({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: unique([taskId, ...getTag(state, tagId).taskIds]),
|
||||
taskIds: unique([taskId, ...getTag(updatedState, tagId).taskIds]),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return updateTags(state, [...removeUpdates, ...addUpdates]);
|
||||
return updateTags(updatedState, [...removeUpdates, ...addUpdates]);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -1,993 +0,0 @@
|
|||
import { Action, ActionReducer, MetaReducer } from '@ngrx/store';
|
||||
import { Update } from '@ngrx/entity';
|
||||
import { RootState } from '../root-state';
|
||||
import { TaskSharedActions } from './task-shared.actions';
|
||||
import {
|
||||
PROJECT_FEATURE_NAME,
|
||||
projectAdapter,
|
||||
} from '../../features/project/store/project.reducer';
|
||||
import { TAG_FEATURE_NAME, tagAdapter } from '../../features/tag/store/tag.reducer';
|
||||
import { TASK_FEATURE_NAME, taskAdapter } from '../../features/tasks/store/task.reducer';
|
||||
import {
|
||||
deleteTaskHelper,
|
||||
removeTaskFromParentSideEffects,
|
||||
updateDoneOnForTask,
|
||||
updateTimeEstimateForTask,
|
||||
updateTimeSpentForTask,
|
||||
} from '../../features/tasks/store/task.reducer.util';
|
||||
import { Tag } from '../../features/tag/tag.model';
|
||||
import { Project } from '../../features/project/project.model';
|
||||
import { DEFAULT_TASK, Task, TaskWithSubTasks } from '../../features/tasks/task.model';
|
||||
import { calcTotalTimeSpent } from '../../features/tasks/util/calc-total-time-spent';
|
||||
import { TODAY_TAG } from '../../features/tag/tag.const';
|
||||
import { getWorklogStr } from '../../util/get-work-log-str';
|
||||
import { unique } from '../../util/unique';
|
||||
import { isToday } from '../../util/is-today.util';
|
||||
import { moveItemBeforeItem } from '../../util/move-item-before-item';
|
||||
import { PlannerActions } from '../../features/planner/store/planner.actions';
|
||||
|
||||
// =============================================================================
|
||||
// TYPES & UTILITIES
|
||||
// =============================================================================
|
||||
|
||||
type ProjectTaskList = 'backlogTaskIds' | 'taskIds';
|
||||
type TaskEntity = { id: string; projectId?: string | null; tagIds?: string[] };
|
||||
type TaskWithTags = Task & { tagIds: string[] };
|
||||
type ActionHandler = (state: RootState) => RootState;
|
||||
type ActionHandlerMap = Record<string, ActionHandler>;
|
||||
|
||||
// =============================================================================
|
||||
// MAIN ACTION HANDLERS
|
||||
// =============================================================================
|
||||
|
||||
// Organized action handler mappings by domain
|
||||
const createActionHandlers = (state: RootState, action: Action): ActionHandlerMap => ({
|
||||
// Task Management
|
||||
[TaskSharedActions.addTask.type]: () => {
|
||||
const { task, isAddToBottom, isAddToBacklog } = action as ReturnType<
|
||||
typeof TaskSharedActions.addTask
|
||||
>;
|
||||
return handleAddTask(state, task, isAddToBottom, isAddToBacklog);
|
||||
},
|
||||
[TaskSharedActions.convertToMainTask.type]: () => {
|
||||
const { task, parentTagIds, isPlanForToday } = action as ReturnType<
|
||||
typeof TaskSharedActions.convertToMainTask
|
||||
>;
|
||||
return handleConvertToMainTask(state, task, parentTagIds, isPlanForToday);
|
||||
},
|
||||
[TaskSharedActions.deleteTask.type]: () => {
|
||||
const { task } = action as ReturnType<typeof TaskSharedActions.deleteTask>;
|
||||
return handleDeleteTask(state, task);
|
||||
},
|
||||
[TaskSharedActions.deleteTasks.type]: () => {
|
||||
const { taskIds } = action as ReturnType<typeof TaskSharedActions.deleteTasks>;
|
||||
return handleDeleteTasks(state, taskIds);
|
||||
},
|
||||
[TaskSharedActions.updateTask.type]: () => {
|
||||
const { task, isIgnoreShortSyntax } = action as ReturnType<
|
||||
typeof TaskSharedActions.updateTask
|
||||
>;
|
||||
return handleUpdateTask(state, task, isIgnoreShortSyntax);
|
||||
},
|
||||
|
||||
// Task Lifecycle
|
||||
[TaskSharedActions.moveToArchive.type]: () => {
|
||||
const { tasks } = action as ReturnType<typeof TaskSharedActions.moveToArchive>;
|
||||
return handleMoveToArchive(state, tasks);
|
||||
},
|
||||
[TaskSharedActions.restoreTask.type]: () => {
|
||||
const { task, subTasks } = action as ReturnType<typeof TaskSharedActions.restoreTask>;
|
||||
return handleRestoreTask(state, task, subTasks);
|
||||
},
|
||||
|
||||
// Task Scheduling
|
||||
[TaskSharedActions.scheduleTaskWithTime.type]: () => {
|
||||
const { task, dueWithTime } = action as ReturnType<
|
||||
typeof TaskSharedActions.scheduleTaskWithTime
|
||||
>;
|
||||
return handleScheduleTaskWithTime(state, task, dueWithTime);
|
||||
},
|
||||
[TaskSharedActions.reScheduleTaskWithTime.type]: () => {
|
||||
const { task, dueWithTime } = action as ReturnType<
|
||||
typeof TaskSharedActions.reScheduleTaskWithTime
|
||||
>;
|
||||
return handleScheduleTaskWithTime(state, task, dueWithTime);
|
||||
},
|
||||
[TaskSharedActions.unscheduleTask.type]: () => {
|
||||
const { id } = action as ReturnType<typeof TaskSharedActions.unscheduleTask>;
|
||||
return handleUnScheduleTask(state, id);
|
||||
},
|
||||
|
||||
// Project Management
|
||||
[TaskSharedActions.moveToOtherProject.type]: () => {
|
||||
const { task, targetProjectId } = action as ReturnType<
|
||||
typeof TaskSharedActions.moveToOtherProject
|
||||
>;
|
||||
return handleMoveToOtherProject(state, task, targetProjectId);
|
||||
},
|
||||
[TaskSharedActions.deleteProject.type]: () => {
|
||||
const { allTaskIds } = action as ReturnType<typeof TaskSharedActions.deleteProject>;
|
||||
return handleDeleteProject(state, allTaskIds);
|
||||
},
|
||||
|
||||
// Today Tag Management
|
||||
[TaskSharedActions.planTasksForToday.type]: () => {
|
||||
const { taskIds, parentTaskMap = {} } = action as ReturnType<
|
||||
typeof TaskSharedActions.planTasksForToday
|
||||
>;
|
||||
return handlePlanTasksForToday(state, taskIds, parentTaskMap);
|
||||
},
|
||||
[TaskSharedActions.removeTasksFromTodayTag.type]: () => {
|
||||
const { taskIds } = action as ReturnType<
|
||||
typeof TaskSharedActions.removeTasksFromTodayTag
|
||||
>;
|
||||
return handleRemoveTasksFromTodayTag(state, taskIds);
|
||||
},
|
||||
[TaskSharedActions.moveTaskInTodayTagList.type]: () => {
|
||||
const { toTaskId, fromTaskId } = action as ReturnType<
|
||||
typeof TaskSharedActions.moveTaskInTodayTagList
|
||||
>;
|
||||
return handleMoveTaskInTodayTagList(state, toTaskId, fromTaskId);
|
||||
},
|
||||
|
||||
// Tag Management
|
||||
[TaskSharedActions.removeTagsForAllTasks.type]: () => {
|
||||
const { tagIdsToRemove } = action as ReturnType<
|
||||
typeof TaskSharedActions.removeTagsForAllTasks
|
||||
>;
|
||||
return handleRemoveTagsForAllTasks(state, tagIdsToRemove);
|
||||
},
|
||||
|
||||
// Planner Actions
|
||||
[PlannerActions.transferTask.type]: () => {
|
||||
const { task, today, targetIndex, newDay, prevDay, targetTaskId } =
|
||||
action as ReturnType<typeof PlannerActions.transferTask>;
|
||||
return handleTransferTask(
|
||||
state,
|
||||
task,
|
||||
today,
|
||||
targetIndex,
|
||||
newDay,
|
||||
prevDay,
|
||||
targetTaskId,
|
||||
);
|
||||
},
|
||||
[PlannerActions.planTaskForDay.type]: () => {
|
||||
const { task, day, isAddToTop } = action as ReturnType<
|
||||
typeof PlannerActions.planTaskForDay
|
||||
>;
|
||||
return handlePlanTaskForDay(state, task, day, isAddToTop || false);
|
||||
},
|
||||
[PlannerActions.moveBeforeTask.type]: () => {
|
||||
const { fromTask, toTaskId } = action as ReturnType<
|
||||
typeof PlannerActions.moveBeforeTask
|
||||
>;
|
||||
return handleMoveBeforeTask(state, fromTask, toTaskId);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Meta-reducer that handles cross-cutting concerns for task, project, and tag state updates
|
||||
*/
|
||||
export const taskSharedMetaReducer: MetaReducer = (
|
||||
reducer: ActionReducer<any, Action>,
|
||||
) => {
|
||||
return (state: unknown, action: Action) => {
|
||||
if (!state) return reducer(state, action);
|
||||
|
||||
const rootState = state as RootState;
|
||||
const actionHandlers = createActionHandlers(rootState, action);
|
||||
const handler = actionHandlers[action.type];
|
||||
const updatedState = handler ? handler(rootState) : rootState;
|
||||
|
||||
return reducer(updatedState, action);
|
||||
};
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// ACTION HANDLERS
|
||||
// =============================================================================
|
||||
|
||||
const handleAddTask = (
|
||||
state: RootState,
|
||||
task: TaskWithTags,
|
||||
isAddToBottom: boolean,
|
||||
isAddToBacklog: boolean,
|
||||
): RootState => {
|
||||
let updatedState = state;
|
||||
|
||||
// Add task to task state
|
||||
const newTask: Task = {
|
||||
...DEFAULT_TASK,
|
||||
...task,
|
||||
timeSpent: calcTotalTimeSpent(task.timeSpentOnDay || {}),
|
||||
projectId: task.projectId || '',
|
||||
};
|
||||
updatedState = {
|
||||
...updatedState,
|
||||
[TASK_FEATURE_NAME]: taskAdapter.addOne(newTask, updatedState[TASK_FEATURE_NAME]),
|
||||
};
|
||||
|
||||
// Update project if task has projectId
|
||||
if (task.projectId && state[PROJECT_FEATURE_NAME].entities[task.projectId]) {
|
||||
const project = getProject(state, task.projectId);
|
||||
const targetList: ProjectTaskList =
|
||||
isAddToBacklog && project.isEnableBacklog ? 'backlogTaskIds' : 'taskIds';
|
||||
|
||||
updatedState = updateProject(updatedState, task.projectId, {
|
||||
[targetList]: addTaskToList(project[targetList], task.id, isAddToBottom),
|
||||
});
|
||||
}
|
||||
|
||||
// Update tags - only update tags that exist
|
||||
const tagIdsToUpdate = [
|
||||
...task.tagIds,
|
||||
...(task.dueDay === getWorklogStr() ? [TODAY_TAG.id] : []),
|
||||
].filter((tagId) => state[TAG_FEATURE_NAME].entities[tagId]);
|
||||
|
||||
const tagUpdates = tagIdsToUpdate.map(
|
||||
(tagId): Update<Tag> => ({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: addTaskToList(getTag(state, tagId).taskIds, task.id, isAddToBottom),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return updateTags(updatedState, tagUpdates);
|
||||
};
|
||||
|
||||
const handleConvertToMainTask = (
|
||||
state: RootState,
|
||||
task: Task,
|
||||
parentTagIds: string[],
|
||||
isPlanForToday?: boolean,
|
||||
): RootState => {
|
||||
// First, get the parent task to copy its properties
|
||||
const parentTask = state[TASK_FEATURE_NAME].entities[task.parentId as string] as Task;
|
||||
if (!parentTask) {
|
||||
throw new Error('No parent for sub task');
|
||||
}
|
||||
|
||||
// Handle parent-child relationship cleanup and task entity updates
|
||||
const taskStateAfterParentCleanup = removeTaskFromParentSideEffects(
|
||||
state[TASK_FEATURE_NAME],
|
||||
task as Task,
|
||||
);
|
||||
|
||||
const updatedTaskState = taskAdapter.updateOne(
|
||||
{
|
||||
id: task.id,
|
||||
changes: {
|
||||
parentId: undefined,
|
||||
tagIds: [...parentTask.tagIds],
|
||||
...(isPlanForToday ? { dueDay: getWorklogStr() } : {}),
|
||||
},
|
||||
},
|
||||
taskStateAfterParentCleanup,
|
||||
);
|
||||
|
||||
let updatedState = {
|
||||
...state,
|
||||
[TASK_FEATURE_NAME]: updatedTaskState,
|
||||
};
|
||||
|
||||
// Update project if task has projectId
|
||||
if (task.projectId && state[PROJECT_FEATURE_NAME].entities[task.projectId]) {
|
||||
const project = getProject(state, task.projectId);
|
||||
updatedState = updateProject(updatedState, task.projectId, {
|
||||
taskIds: [task.id, ...project.taskIds],
|
||||
});
|
||||
}
|
||||
|
||||
// Update tags - only update tags that exist
|
||||
const tagIdsToUpdate = [
|
||||
...parentTagIds,
|
||||
...(isPlanForToday ? [TODAY_TAG.id] : []),
|
||||
].filter((tagId) => state[TAG_FEATURE_NAME].entities[tagId]);
|
||||
const tagUpdates = tagIdsToUpdate.map(
|
||||
(tagId): Update<Tag> => ({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: [task.id, ...getTag(updatedState, tagId).taskIds],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return updateTags(updatedState, tagUpdates);
|
||||
};
|
||||
|
||||
const handleDeleteTask = (
|
||||
state: RootState,
|
||||
task: {
|
||||
id: string;
|
||||
projectId?: string | null;
|
||||
tagIds: string[];
|
||||
subTasks?: Task[];
|
||||
subTaskIds?: string[];
|
||||
},
|
||||
): RootState => {
|
||||
let updatedState = state;
|
||||
|
||||
// Delete task from task state using helper
|
||||
updatedState = {
|
||||
...updatedState,
|
||||
[TASK_FEATURE_NAME]: deleteTaskHelper(
|
||||
updatedState[TASK_FEATURE_NAME],
|
||||
task as TaskWithSubTasks,
|
||||
),
|
||||
};
|
||||
|
||||
// Update project if task has projectId
|
||||
if (task.projectId && state[PROJECT_FEATURE_NAME].entities[task.projectId]) {
|
||||
const project = getProject(state, task.projectId);
|
||||
updatedState = updateProject(updatedState, task.projectId, {
|
||||
taskIds: removeTasksFromList(project.taskIds, [task.id]),
|
||||
backlogTaskIds: removeTasksFromList(project.backlogTaskIds, [task.id]),
|
||||
});
|
||||
}
|
||||
|
||||
// Update tags - collect all affected tags and tasks to remove
|
||||
// Only include tags that actually exist in the state to prevent errors
|
||||
const potentialTagIds = [
|
||||
TODAY_TAG.id, // always check today list
|
||||
...task.tagIds,
|
||||
...(task.subTasks || []).flatMap((st) => st.tagIds || []),
|
||||
];
|
||||
|
||||
const affectedTagIds = unique(
|
||||
potentialTagIds.filter((tagId) => state[TAG_FEATURE_NAME].entities[tagId]),
|
||||
);
|
||||
|
||||
const taskIdsToRemove = [task.id, ...(task.subTaskIds || [])];
|
||||
|
||||
const tagUpdates = affectedTagIds.map(
|
||||
(tagId): Update<Tag> => ({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: removeTasksFromList(getTag(state, tagId).taskIds, taskIdsToRemove),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return updateTags(updatedState, tagUpdates);
|
||||
};
|
||||
|
||||
const handleDeleteTasks = (state: RootState, taskIds: string[]): RootState => {
|
||||
let updatedState = state;
|
||||
|
||||
// Get all task IDs including subtasks
|
||||
const allIds = taskIds.reduce((acc: string[], id: string) => {
|
||||
const task = state[TASK_FEATURE_NAME].entities[id] as Task;
|
||||
return task ? [...acc, id, ...task.subTaskIds] : [...acc, id];
|
||||
}, []);
|
||||
|
||||
// Remove tasks from task state
|
||||
const newTaskState = taskAdapter.removeMany(allIds, updatedState[TASK_FEATURE_NAME]);
|
||||
updatedState = {
|
||||
...updatedState,
|
||||
[TASK_FEATURE_NAME]: {
|
||||
...newTaskState,
|
||||
currentTaskId:
|
||||
newTaskState.currentTaskId && taskIds.includes(newTaskState.currentTaskId)
|
||||
? null
|
||||
: newTaskState.currentTaskId,
|
||||
},
|
||||
};
|
||||
|
||||
// Only update tags that actually contain at least one of the tasks being deleted
|
||||
const affectedTags = (state[TAG_FEATURE_NAME].ids as string[]).filter((tagId) => {
|
||||
const tag = state[TAG_FEATURE_NAME].entities[tagId];
|
||||
if (!tag) return false;
|
||||
return taskIds.some((taskId) => tag.taskIds.includes(taskId));
|
||||
});
|
||||
|
||||
const tagUpdates = affectedTags.map(
|
||||
(tagId): Update<Tag> => ({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: removeTasksFromList(getTag(state, tagId).taskIds, taskIds),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return updateTags(updatedState, tagUpdates);
|
||||
};
|
||||
|
||||
const handleMoveToArchive = (state: RootState, tasks: TaskWithSubTasks[]): RootState => {
|
||||
const taskIdsToArchive = tasks.flatMap((t) => [t.id, ...t.subTasks.map((st) => st.id)]);
|
||||
|
||||
// Update projects
|
||||
const projectIds = unique(
|
||||
tasks.map((t) => t.projectId).filter((pid): pid is string => !!pid),
|
||||
);
|
||||
|
||||
let updatedState = state;
|
||||
|
||||
if (projectIds.length > 0) {
|
||||
const projectUpdates = projectIds.map((pid): Update<Project> => {
|
||||
const project = getProject(state, pid);
|
||||
return {
|
||||
id: pid,
|
||||
changes: {
|
||||
taskIds: removeTasksFromList(project.taskIds, taskIdsToArchive),
|
||||
backlogTaskIds: removeTasksFromList(project.backlogTaskIds, taskIdsToArchive),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
updatedState = {
|
||||
...updatedState,
|
||||
[PROJECT_FEATURE_NAME]: projectAdapter.updateMany(
|
||||
projectUpdates,
|
||||
updatedState[PROJECT_FEATURE_NAME],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Update tags
|
||||
const affectedTagIds = unique([
|
||||
TODAY_TAG.id, // always cleanup today tag
|
||||
...tasks.flatMap((t) => [...t.tagIds, ...t.subTasks.flatMap((st) => st.tagIds)]),
|
||||
]);
|
||||
|
||||
const tagUpdates = affectedTagIds.map(
|
||||
(tagId): Update<Tag> => ({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: removeTasksFromList(getTag(state, tagId).taskIds, taskIdsToArchive),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return updateTags(updatedState, tagUpdates);
|
||||
};
|
||||
|
||||
const handleRestoreTask = (
|
||||
state: RootState,
|
||||
task: TaskEntity,
|
||||
subTasks: Task[],
|
||||
): RootState => {
|
||||
// First, restore the task entities with proper state
|
||||
const restoredTask = {
|
||||
...task,
|
||||
isDone: false,
|
||||
doneOn: undefined,
|
||||
};
|
||||
|
||||
const updatedTaskState = taskAdapter.addMany(
|
||||
[restoredTask as Task, ...subTasks],
|
||||
state[TASK_FEATURE_NAME],
|
||||
);
|
||||
|
||||
let updatedState = {
|
||||
...state,
|
||||
[TASK_FEATURE_NAME]: updatedTaskState,
|
||||
};
|
||||
|
||||
// Update project if task has projectId
|
||||
if (task.projectId) {
|
||||
const project = getProject(state, task.projectId);
|
||||
updatedState = updateProject(updatedState, task.projectId, {
|
||||
taskIds: unique([...project.taskIds, task.id]),
|
||||
});
|
||||
}
|
||||
|
||||
// Update tags - group tasks by tagId
|
||||
const allTasks = [task, ...subTasks];
|
||||
const tagTaskMap = allTasks.reduce(
|
||||
(map, t) => {
|
||||
(t.tagIds || []).forEach((tagId) => {
|
||||
if (!map[tagId]) map[tagId] = [];
|
||||
map[tagId].push(t.id);
|
||||
});
|
||||
return map;
|
||||
},
|
||||
{} as Record<string, string[]>,
|
||||
);
|
||||
|
||||
const tagUpdates = Object.entries(tagTaskMap)
|
||||
.filter(([tagId]) => state[TAG_FEATURE_NAME].entities[tagId]) // Only update existing tags
|
||||
.map(
|
||||
([tagId, taskIds]): Update<Tag> => ({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: unique([...getTag(updatedState, tagId).taskIds, ...taskIds]),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return updateTags(updatedState, tagUpdates);
|
||||
};
|
||||
|
||||
const handleScheduleTaskWithTime = (
|
||||
state: RootState,
|
||||
task: { id: string },
|
||||
dueWithTime: number,
|
||||
): RootState => {
|
||||
// Check if task already has the same dueWithTime
|
||||
const currentTask = state[TASK_FEATURE_NAME].entities[task.id] as Task;
|
||||
if (!currentTask) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const todayTag = getTag(state, TODAY_TAG.id);
|
||||
const isScheduledForToday = isToday(dueWithTime);
|
||||
const isCurrentlyInToday = todayTag.taskIds.includes(task.id);
|
||||
|
||||
// If task is already correctly scheduled, don't change state
|
||||
if (
|
||||
currentTask.dueWithTime === dueWithTime &&
|
||||
isScheduledForToday === isCurrentlyInToday
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// First, update the task entity with the scheduling data
|
||||
const updatedState = {
|
||||
...state,
|
||||
[TASK_FEATURE_NAME]: taskAdapter.updateOne(
|
||||
{
|
||||
id: task.id,
|
||||
changes: {
|
||||
dueWithTime,
|
||||
dueDay: undefined,
|
||||
},
|
||||
},
|
||||
state[TASK_FEATURE_NAME],
|
||||
),
|
||||
};
|
||||
|
||||
// No tag change needed
|
||||
if (isScheduledForToday === isCurrentlyInToday) {
|
||||
return updatedState;
|
||||
}
|
||||
|
||||
const newTaskIds = isScheduledForToday
|
||||
? unique([task.id, ...todayTag.taskIds]) // Add to top, prevent duplicates
|
||||
: todayTag.taskIds.filter((id) => id !== task.id); // Remove
|
||||
|
||||
return updateTags(updatedState, [
|
||||
{
|
||||
id: TODAY_TAG.id,
|
||||
changes: { taskIds: newTaskIds },
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleUnScheduleTask = (state: RootState, taskId: string): RootState => {
|
||||
// First, update the task entity to clear scheduling data
|
||||
const updatedState = {
|
||||
...state,
|
||||
[TASK_FEATURE_NAME]: taskAdapter.updateOne(
|
||||
{
|
||||
id: taskId,
|
||||
changes: {
|
||||
dueDay: undefined,
|
||||
dueWithTime: undefined,
|
||||
},
|
||||
},
|
||||
state[TASK_FEATURE_NAME],
|
||||
),
|
||||
};
|
||||
|
||||
// Then, handle today tag updates
|
||||
const todayTag = getTag(updatedState, TODAY_TAG.id);
|
||||
|
||||
if (!todayTag.taskIds.includes(taskId)) {
|
||||
return updatedState;
|
||||
}
|
||||
|
||||
return updateTags(updatedState, [
|
||||
{
|
||||
id: TODAY_TAG.id,
|
||||
changes: {
|
||||
taskIds: todayTag.taskIds.filter((id) => id !== taskId),
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleUpdateTask = (
|
||||
state: RootState,
|
||||
taskUpdate: Update<Task>,
|
||||
isIgnoreShortSyntax?: boolean,
|
||||
): RootState => {
|
||||
const taskId = taskUpdate.id as string;
|
||||
const currentTask = state[TASK_FEATURE_NAME].entities[taskId] as Task;
|
||||
|
||||
if (!currentTask) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let updatedState = state;
|
||||
|
||||
// Handle tag changes if tagIds are being updated
|
||||
if (taskUpdate.changes.tagIds) {
|
||||
const oldTagIds = currentTask.tagIds;
|
||||
const newTagIds = taskUpdate.changes.tagIds;
|
||||
|
||||
updatedState = handleTagUpdates(updatedState, taskId, oldTagIds, newTagIds);
|
||||
}
|
||||
|
||||
// Handle task state updates using existing task reducer logic
|
||||
let taskState = updatedState[TASK_FEATURE_NAME];
|
||||
const { timeSpentOnDay, timeEstimate } = taskUpdate.changes;
|
||||
|
||||
taskState = timeSpentOnDay
|
||||
? updateTimeSpentForTask(taskId, timeSpentOnDay, taskState)
|
||||
: taskState;
|
||||
taskState = updateTimeEstimateForTask(taskUpdate, timeEstimate, taskState);
|
||||
taskState = updateDoneOnForTask(taskUpdate, taskState);
|
||||
taskState = taskAdapter.updateOne(taskUpdate, taskState);
|
||||
|
||||
return {
|
||||
...updatedState,
|
||||
[TASK_FEATURE_NAME]: taskState,
|
||||
};
|
||||
};
|
||||
|
||||
const handleTagUpdates = (
|
||||
state: RootState,
|
||||
taskId: string,
|
||||
oldTagIds: string[],
|
||||
newTagIds: string[],
|
||||
): RootState => {
|
||||
const tagsToRemoveFrom = oldTagIds
|
||||
.filter((oldId) => !newTagIds.includes(oldId))
|
||||
.filter((tagId) => state[TAG_FEATURE_NAME].entities[tagId]); // Only existing tags
|
||||
const tagsToAddTo = newTagIds
|
||||
.filter((newId) => !oldTagIds.includes(newId))
|
||||
.filter((tagId) => state[TAG_FEATURE_NAME].entities[tagId]); // Only existing tags
|
||||
|
||||
const removeUpdates = tagsToRemoveFrom.map(
|
||||
(tagId): Update<Tag> => ({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: getTag(state, tagId).taskIds.filter((id) => id !== taskId),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const addUpdates = tagsToAddTo.map(
|
||||
(tagId): Update<Tag> => ({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: unique([taskId, ...getTag(state, tagId).taskIds]),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return updateTags(state, [...removeUpdates, ...addUpdates]);
|
||||
};
|
||||
|
||||
const handleMoveToOtherProject = (
|
||||
state: RootState,
|
||||
task: TaskWithSubTasks,
|
||||
targetProjectId: string,
|
||||
): RootState => {
|
||||
const currentProjectId = task.projectId;
|
||||
const allTaskIds = [task.id, ...task.subTaskIds];
|
||||
|
||||
let updatedState = state;
|
||||
|
||||
// Remove tasks from current project if it exists
|
||||
if (currentProjectId && state[PROJECT_FEATURE_NAME].entities[currentProjectId]) {
|
||||
const currentProject = getProject(state, currentProjectId);
|
||||
updatedState = updateProject(updatedState, currentProjectId, {
|
||||
taskIds: removeTasksFromList(currentProject.taskIds, allTaskIds),
|
||||
backlogTaskIds: removeTasksFromList(currentProject.backlogTaskIds, allTaskIds),
|
||||
});
|
||||
}
|
||||
|
||||
// Add tasks to target project
|
||||
if (state[PROJECT_FEATURE_NAME].entities[targetProjectId]) {
|
||||
const targetProject = getProject(updatedState, targetProjectId);
|
||||
// Add all tasks to the regular task list (not backlog) when moving projects
|
||||
// This ensures tasks are visible in the new project
|
||||
updatedState = updateProject(updatedState, targetProjectId, {
|
||||
taskIds: unique([...targetProject.taskIds, ...allTaskIds]),
|
||||
});
|
||||
}
|
||||
|
||||
// Update all tasks with new projectId
|
||||
const taskUpdates: Update<Task>[] = allTaskIds.map((id) => ({
|
||||
id,
|
||||
changes: {
|
||||
projectId: targetProjectId,
|
||||
},
|
||||
}));
|
||||
|
||||
updatedState = {
|
||||
...updatedState,
|
||||
[TASK_FEATURE_NAME]: taskAdapter.updateMany(
|
||||
taskUpdates,
|
||||
updatedState[TASK_FEATURE_NAME],
|
||||
),
|
||||
};
|
||||
|
||||
return updatedState;
|
||||
};
|
||||
|
||||
const handleDeleteProject = (state: RootState, allTaskIds: string[]): RootState => {
|
||||
const tagUpdates = (state[TAG_FEATURE_NAME].ids as string[]).map(
|
||||
(tagId): Update<Tag> => ({
|
||||
id: tagId,
|
||||
changes: {
|
||||
taskIds: removeTasksFromList(getTag(state, tagId).taskIds, allTaskIds),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return updateTags(state, tagUpdates);
|
||||
};
|
||||
|
||||
const handlePlanTasksForToday = (
|
||||
state: RootState,
|
||||
taskIds: string[],
|
||||
parentTaskMap: Record<string, string | undefined>,
|
||||
): RootState => {
|
||||
const todayTag = getTag(state, TODAY_TAG.id);
|
||||
const today = getWorklogStr();
|
||||
|
||||
// Filter out tasks that are already in today or whose parent is in today
|
||||
const newTasksForToday = taskIds.filter((taskId) => {
|
||||
if (todayTag.taskIds.includes(taskId)) return false;
|
||||
const parentId = parentTaskMap[taskId];
|
||||
return !parentId || !todayTag.taskIds.includes(parentId);
|
||||
});
|
||||
|
||||
// First, update the task entities with dueDay
|
||||
const taskUpdates: Update<Task>[] = taskIds.map((taskId) => ({
|
||||
id: taskId,
|
||||
changes: {
|
||||
dueDay: today,
|
||||
},
|
||||
}));
|
||||
|
||||
const updatedState = {
|
||||
...state,
|
||||
[TASK_FEATURE_NAME]: taskAdapter.updateMany(taskUpdates, state[TASK_FEATURE_NAME]),
|
||||
};
|
||||
|
||||
// Then, update the today tag
|
||||
return updateTags(updatedState, [
|
||||
{
|
||||
id: TODAY_TAG.id,
|
||||
changes: {
|
||||
taskIds: unique([...newTasksForToday, ...todayTag.taskIds]),
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleRemoveTasksFromTodayTag = (
|
||||
state: RootState,
|
||||
taskIds: string[],
|
||||
): RootState => {
|
||||
const todayTag = getTag(state, TODAY_TAG.id);
|
||||
|
||||
return updateTags(state, [
|
||||
{
|
||||
id: TODAY_TAG.id,
|
||||
changes: {
|
||||
taskIds: removeTasksFromList(todayTag.taskIds, taskIds),
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleRemoveTagsForAllTasks = (
|
||||
state: RootState,
|
||||
tagIdsToRemove: string[],
|
||||
): RootState => {
|
||||
const taskState = state[TASK_FEATURE_NAME];
|
||||
|
||||
// Update all tasks to remove the specified tags
|
||||
const taskUpdates: Update<Task>[] = [];
|
||||
Object.values(taskState.entities).forEach((task) => {
|
||||
if (task && task.tagIds && task.tagIds.some((id) => tagIdsToRemove.includes(id))) {
|
||||
taskUpdates.push({
|
||||
id: task.id,
|
||||
changes: {
|
||||
tagIds: task.tagIds.filter((id) => !tagIdsToRemove.includes(id)),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
[TASK_FEATURE_NAME]: taskAdapter.updateMany(taskUpdates, taskState),
|
||||
};
|
||||
};
|
||||
|
||||
const handleTransferTask = (
|
||||
state: RootState,
|
||||
task: Task,
|
||||
today: string,
|
||||
targetIndex: number,
|
||||
newDay: string,
|
||||
prevDay: string,
|
||||
targetTaskId?: string,
|
||||
): RootState => {
|
||||
const todayTag = getTag(state, TODAY_TAG.id);
|
||||
|
||||
if (prevDay === today && newDay !== today) {
|
||||
return updateTags(state, [
|
||||
{
|
||||
id: TODAY_TAG.id,
|
||||
changes: {
|
||||
taskIds: todayTag.taskIds.filter((id) => id !== task.id),
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if (prevDay !== today && newDay === today) {
|
||||
const taskIds = [...todayTag.taskIds];
|
||||
const targetIndexToUse = targetTaskId
|
||||
? todayTag.taskIds.findIndex((id) => id === targetTaskId)
|
||||
: targetIndex;
|
||||
taskIds.splice(targetIndexToUse, 0, task.id);
|
||||
|
||||
return updateTags(state, [
|
||||
{
|
||||
id: TODAY_TAG.id,
|
||||
changes: {
|
||||
taskIds: unique(taskIds),
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const handlePlanTaskForDay = (
|
||||
state: RootState,
|
||||
task: Task,
|
||||
day: string,
|
||||
isAddToTop: boolean,
|
||||
): RootState => {
|
||||
const todayStr = getWorklogStr();
|
||||
const todayTag = getTag(state, TODAY_TAG.id);
|
||||
|
||||
if (day === todayStr && !todayTag.taskIds.includes(task.id)) {
|
||||
return updateTags(state, [
|
||||
{
|
||||
id: todayTag.id,
|
||||
changes: {
|
||||
taskIds: unique(
|
||||
isAddToTop
|
||||
? [task.id, ...todayTag.taskIds]
|
||||
: [...todayTag.taskIds.filter((tid) => tid !== task.id), task.id],
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
} else if (day !== todayStr && todayTag.taskIds.includes(task.id)) {
|
||||
return updateTags(state, [
|
||||
{
|
||||
id: todayTag.id,
|
||||
changes: {
|
||||
taskIds: todayTag.taskIds.filter((id) => id !== task.id),
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const handleMoveBeforeTask = (
|
||||
state: RootState,
|
||||
fromTask: Task,
|
||||
toTaskId: string,
|
||||
): RootState => {
|
||||
const todayTag = getTag(state, TODAY_TAG.id);
|
||||
|
||||
if (todayTag.taskIds.includes(toTaskId)) {
|
||||
const taskIds = todayTag.taskIds.filter((id) => id !== fromTask.id);
|
||||
const targetIndex = taskIds.indexOf(toTaskId);
|
||||
taskIds.splice(targetIndex, 0, fromTask.id);
|
||||
|
||||
return updateTags(state, [
|
||||
{
|
||||
id: todayTag.id,
|
||||
changes: {
|
||||
taskIds: unique(taskIds),
|
||||
},
|
||||
},
|
||||
]);
|
||||
} else if (todayTag.taskIds.includes(fromTask.id)) {
|
||||
return updateTags(state, [
|
||||
{
|
||||
id: todayTag.id,
|
||||
changes: {
|
||||
taskIds: todayTag.taskIds.filter((id) => id !== fromTask.id),
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const handleMoveTaskInTodayTagList = (
|
||||
state: RootState,
|
||||
toTaskId: string,
|
||||
fromTaskId: string,
|
||||
): RootState => {
|
||||
const todayTag = getTag(state, TODAY_TAG.id);
|
||||
|
||||
// If either task is not in the Today list, don't perform the move
|
||||
if (!todayTag.taskIds.includes(fromTaskId) || !todayTag.taskIds.includes(toTaskId)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return updateTags(state, [
|
||||
{
|
||||
id: todayTag.id,
|
||||
changes: {
|
||||
taskIds: moveItemBeforeItem(todayTag.taskIds, fromTaskId, toTaskId),
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// SHARED HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
// State update helpers
|
||||
const updateProject = (
|
||||
state: RootState,
|
||||
projectId: string,
|
||||
changes: Partial<Project>,
|
||||
): RootState => ({
|
||||
...state,
|
||||
[PROJECT_FEATURE_NAME]: projectAdapter.updateOne(
|
||||
{ id: projectId, changes },
|
||||
state[PROJECT_FEATURE_NAME],
|
||||
),
|
||||
});
|
||||
|
||||
const updateTags = (state: RootState, updates: Update<Tag>[]): RootState => ({
|
||||
...state,
|
||||
[TAG_FEATURE_NAME]: tagAdapter.updateMany(updates, state[TAG_FEATURE_NAME]),
|
||||
});
|
||||
|
||||
// Entity getters
|
||||
const getTag = (state: RootState, tagId: string): Tag =>
|
||||
state[TAG_FEATURE_NAME].entities[tagId] as Tag;
|
||||
|
||||
const getProject = (state: RootState, projectId: string): Project =>
|
||||
state[PROJECT_FEATURE_NAME].entities[projectId] as Project;
|
||||
|
||||
// List manipulation helpers
|
||||
const addTaskToList = (
|
||||
taskIds: string[],
|
||||
taskId: string,
|
||||
isAddToBottom: boolean,
|
||||
): string[] => (isAddToBottom ? [...taskIds, taskId] : [taskId, ...taskIds]);
|
||||
|
||||
const removeTasksFromList = (taskIds: string[], toRemove: string[]): string[] =>
|
||||
taskIds.filter((id) => !toRemove.includes(id));
|
||||
|
||||
// Today tag specific helpers (currently unused but kept for potential future use)
|
||||
// const addTaskToTodayTag = (state: RootState, taskId: string, isAddToTop = false): RootState => {
|
||||
// const todayTag = getTag(state, TODAY_TAG.id);
|
||||
// const newTaskIds = isAddToTop
|
||||
// ? addToTopOfList(todayTag.taskIds, taskId)
|
||||
// : addToBottomOfList(todayTag.taskIds, taskId);
|
||||
// return updateTodayTag(state, newTaskIds);
|
||||
// };
|
||||
|
||||
// const removeTaskFromTodayTag = (state: RootState, taskId: string): RootState => {
|
||||
// const todayTag = getTag(state, TODAY_TAG.id);
|
||||
// return updateTodayTag(state, removeTasksFromList(todayTag.taskIds, [taskId]));
|
||||
// };
|
||||
|
||||
// const isTaskInTodayTag = (state: RootState, taskId: string): boolean =>
|
||||
// getTag(state, TODAY_TAG.id).taskIds.includes(taskId);
|
||||
Loading…
Add table
Add a link
Reference in a new issue