import { createActionGroup } from '@ngrx/store'; import { Update } from '@ngrx/entity'; import { Task, TaskWithSubTasks } from '../../features/tasks/task.model'; import { IssueDataReduced } from '../../features/issue/issue.model'; import { WorkContextType } from '../../features/work-context/work-context.model'; import { BatchOperation } from '@super-productivity/plugin-api'; import { PersistentActionMeta } from '../../op-log/core/persistent-action.interface'; import { OpType } from '../../op-log/core/operation.types'; /** * Payload marker stamped on every new `deleteProject` operation so the LWW * conflict planner can give it delete-wins precedence. Shared with the sync * classifier (`conflict-resolution.service.ts`) so a rename is compiler-checked * on both ends — dropping/renaming it silently would regress to resurrecting an * empty project (see ARCHITECTURE-DECISIONS.md #7). */ export const PROJECT_DELETE_WINS_MARKER = 'projectDeleteWins'; /** * Shared actions that affect multiple reducers (tasks, projects, tags) * These actions are handled by the task-shared meta-reducer */ export const TaskSharedActions = createActionGroup({ source: 'Task Shared', events: { // Task Management addTask: (taskProps: { task: Task; issue?: IssueDataReduced; workContextId: string; workContextType: WorkContextType; isAddToBacklog: boolean; isAddToBottom: boolean; isIgnoreShortSyntax?: boolean; autoPlanToday?: string; autoPlanStartOfNextDayDiffMs?: number; isExampleTask?: boolean; }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.task.id, opType: OpType.Create, } satisfies PersistentActionMeta, }), convertToMainTask: (taskProps: { task: Task; parentTagIds?: string[]; isPlanForToday?: boolean; afterTaskId?: string | null; isDone?: boolean; today?: string; doneOn?: number; modified?: number; }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.task.id, opType: OpType.Update, } satisfies PersistentActionMeta, }), convertToSubTask: (taskProps: { taskId: string; targetParentId: string; afterTaskId: string | null; }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.taskId, opType: OpType.Update, } satisfies PersistentActionMeta, }), deleteTask: (taskProps: { task: TaskWithSubTasks }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.task.id, opType: OpType.Delete, } satisfies PersistentActionMeta, }), // Issue metadata for remote issue deletion still travels through // DeletedTaskIssueSidecarService. Task snapshots are persisted separately // so a concurrent winning update can recreate an entity after this delete. deleteTasks: (taskProps: { taskIds: string[]; tasks?: Task[] }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityIds: taskProps.taskIds, opType: OpType.Delete, isBulk: true, } satisfies PersistentActionMeta, }), // TODO rename to `moveTaskToArchive__` to indicate it should not be called directly // Note: Full task payload is required for sync reliability. // Remote clients need task data to write to their local archive. // See docs/archive-operation-redesign.md for detailed analysis. moveToArchive: (taskProps: { tasks: TaskWithSubTasks[] }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityIds: taskProps.tasks.map((t) => t.id), opType: OpType.Update, isBulk: true, } satisfies PersistentActionMeta, }), restoreTask: (taskProps: { task: Task | TaskWithSubTasks; subTasks: Task[] }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.task.id, opType: OpType.Update, } satisfies PersistentActionMeta, }), // Restore a deleted task (undo delete) - syncs across devices restoreDeletedTask: (payload: { task: TaskWithSubTasks; projectContext?: { projectId: string; taskIdsForProject: string[]; taskIdsForProjectBacklog: string[]; }; parentContext?: { parentTaskId: string; subTaskIds: string[]; }; tagTaskIdMap: Record; deletedTaskEntities: Record; }) => ({ ...payload, meta: { isPersistent: true, entityType: 'TASK', entityId: payload.task.id, opType: OpType.Update, } satisfies PersistentActionMeta, }), // Task Scheduling scheduleTaskWithTime: (taskProps: { task: Task; dueWithTime: number; remindAt?: number; isMoveToBacklog: boolean; isSkipAutoRemoveFromToday?: boolean; }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.task.id, opType: OpType.Update, } satisfies PersistentActionMeta, }), reScheduleTaskWithTime: (taskProps: { task: Task; dueWithTime: number; remindAt?: number; isMoveToBacklog: boolean; isSkipAutoRemoveFromToday?: boolean; }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.task.id, opType: OpType.Update, } satisfies PersistentActionMeta, }), unscheduleTask: (taskProps: { id: string; isSkipToast?: boolean; isLeaveInToday?: boolean; today?: string; }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.id, opType: OpType.Update, } satisfies PersistentActionMeta, }), dismissReminderOnly: (taskProps: { id: string }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.id, opType: OpType.Update, } satisfies PersistentActionMeta, }), // Task Deadlines setDeadline: (taskProps: { taskId: string; deadlineDay?: string; deadlineWithTime?: number; deadlineRemindAt?: number; autoPlanToday?: string; autoPlanStartOfNextDayDiffMs?: number; }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.taskId, opType: OpType.Update, } satisfies PersistentActionMeta, }), planDeadlineTasksForToday: (taskProps: { taskIds: string[]; today: string; startOfNextDayDiffMs: number; }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityIds: taskProps.taskIds, opType: OpType.Update, isBulk: true, } satisfies PersistentActionMeta, }), removeDeadline: (taskProps: { taskId: string }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.taskId, opType: OpType.Update, } satisfies PersistentActionMeta, }), clearDeadlineReminder: (taskProps: { taskId: string }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.taskId, opType: OpType.Update, } satisfies PersistentActionMeta, }), // Task Updates updateTask: (taskProps: { task: Update; isIgnoreShortSyntax?: boolean; projectMoveSubTaskIds?: string[]; }) => { const entityId = taskProps.task.id as string; return { ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId, ...(taskProps.projectMoveSubTaskIds !== undefined && { entityIds: Array.from( new Set([entityId, ...taskProps.projectMoveSubTaskIds]), ), }), opType: OpType.Update, } satisfies PersistentActionMeta, }; }, // Bulk task update - creates single operation instead of N operations // Critical for repeating task config updates that affect many archived instances updateTasks: (taskProps: { tasks: Update[] }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityIds: taskProps.tasks.map((t) => t.id as string), opType: OpType.Update, isBulk: true, } satisfies PersistentActionMeta, }), // Project Management moveToOtherProject: (taskProps: { task: TaskWithSubTasks; targetProjectId: string; }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityId: taskProps.task.id, opType: OpType.Update, } satisfies PersistentActionMeta, }), deleteProject: (taskProps: { projectId: string; noteIds: string[]; allTaskIds: string[]; }) => ({ ...taskProps, [PROJECT_DELETE_WINS_MARKER]: true as const, meta: { isPersistent: true, entityType: 'PROJECT', entityId: taskProps.projectId, opType: OpType.Delete, } satisfies PersistentActionMeta, }), // Today Tag Management planTasksForToday: (taskProps: { taskIds: string[]; // The logical day "today" referred to when the action was created. // Optional only for legacy operation replay and older tests. today?: string; startOfNextDayDiffMs?: number; parentTaskMap?: { [taskId: string]: string | undefined }; isShowSnack?: boolean; isSkipRemoveReminder?: boolean; isClearScheduledTime?: boolean; }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityIds: taskProps.taskIds, opType: OpType.Update, isBulk: true, } satisfies PersistentActionMeta, }), removeTasksFromTodayTag: (taskProps: { taskIds: string[] }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityIds: taskProps.taskIds, opType: OpType.Update, isBulk: true, } satisfies PersistentActionMeta, }), // Non-persistent variant for automated day-change overdue removal. // Every device runs removeOverdueFormToday$ independently, so syncing this // operation is redundant and causes LWW conflicts with user actions on other // devices (see #6992). The meta-reducer handles it identically to // removeTasksFromTodayTag but the operation log ignores it. localRemoveOverdueFromToday: (taskProps: { taskIds: string[] }) => ({ ...taskProps, }), // Tag Management addTagToTask: (props: { tagId: string; taskId: string }) => ({ ...props, meta: { isPersistent: true, entityType: 'TASK', entityIds: [props.taskId, props.tagId], opType: OpType.Update, } satisfies PersistentActionMeta, }), removeTagsForAllTasks: (taskProps: { tagIdsToRemove: string[] }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TAG', entityIds: taskProps.tagIdsToRemove, opType: OpType.Update, isBulk: true, } satisfies PersistentActionMeta, }), moveTaskInTodayTagList: (taskProps: { toTaskId: string; fromTaskId: string }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'TASK', entityIds: [taskProps.toTaskId, taskProps.fromTaskId], opType: OpType.Move, isBulk: true, } satisfies PersistentActionMeta, }), // Batch Operations batchUpdateForProject: (taskProps: { projectId: string; operations: BatchOperation[]; createdTaskIds: { [tempId: string]: string }; /** Captured before dispatch so replay creates identical task state. */ createdTaskTimestamp?: number; }) => ({ ...taskProps, meta: { isPersistent: true, entityType: 'PROJECT', entityId: taskProps.projectId, opType: OpType.Batch, } satisfies PersistentActionMeta, }), // Issue Provider Management deleteIssueProvider: (props: { issueProviderId: string; taskIdsToUnlink: string[]; }) => ({ ...props, meta: { isPersistent: true, entityType: 'ISSUE_PROVIDER', entityId: props.issueProviderId, opType: OpType.Delete, } satisfies PersistentActionMeta, }), deleteIssueProviders: (props: { ids: string[]; taskIdsToUnlink: string[] }) => ({ ...props, meta: { isPersistent: true, entityType: 'ISSUE_PROVIDER', entityIds: props.ids, opType: OpType.Delete, isBulk: true, } satisfies PersistentActionMeta, }), // Task Repeat Config Management deleteTaskRepeatCfg: (props: { taskRepeatCfgId: string }) => ({ ...props, meta: { isPersistent: true, entityType: 'TASK_REPEAT_CFG', entityId: props.taskRepeatCfgId, opType: OpType.Delete, } satisfies PersistentActionMeta, }), // Short Syntax - Atomic compound action // Combines task updates, project moves, and scheduling into one atomic operation applyShortSyntax: (props: { task: Task; taskChanges: Partial; targetProjectId?: string; schedulingInfo?: { day?: string; isAddToTop?: boolean; dueWithTime?: number; remindAt?: number | null; isMoveToBacklog?: boolean; }; autoPlanToday?: string; autoPlanStartOfNextDayDiffMs?: number; }) => ({ ...props, meta: { isPersistent: true, entityType: 'TASK', entityId: props.task.id, opType: OpType.Update, } satisfies PersistentActionMeta, }), }, });