mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
test(oplog): add persistent-action tests and fix compilation/lint errors
- Add unit tests for isPersistentAction type guard. - Fix compilation errors in task scheduling components caused by removed reminderId/removeReminderFromTask. - Fix type error in create-sorted-blocker-blocks.spec.ts. - Fix lint errors in various files.
This commit is contained in:
parent
b383025fc1
commit
3129c1dbca
30 changed files with 465 additions and 630 deletions
80
docs/refactoring/reminder-refactoring-plan.md
Normal file
80
docs/refactoring/reminder-refactoring-plan.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Reminder Refactoring Plan
|
||||
|
||||
## Objective
|
||||
|
||||
Refactor the Reminder system to eliminate the standalone `reminders` model and persistence file. Instead, reminder data (specifically `remindAt`) will be stored directly on the `Task` model. Reminders for Notes will be discontinued.
|
||||
|
||||
## Motivation
|
||||
|
||||
1. **Operation Log Consistency**: Currently, reminders are stored independently and changes bypass the main NgRx/Operation Log system used for sync. Integrating them into `Task` ensures all reminder changes are properly tracked and synced.
|
||||
2. **Performance**: Reducing the number of files synced (removing `reminders.json`) improves performance, especially for WebDAV users.
|
||||
3. **Simplicity**: Reduces complexity by removing an entire state slice and its associated management logic.
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
### 1. Data Model Changes
|
||||
|
||||
**Remove**:
|
||||
|
||||
- `Reminder` model (standalone entity).
|
||||
- `RecurringConfig` (found to be unused).
|
||||
- `Note` reminders support.
|
||||
|
||||
**Update**:
|
||||
|
||||
- **Task Model**:
|
||||
- Remove `reminderId: string`.
|
||||
- Add `remindAt: number | null` (timestamp).
|
||||
|
||||
### 2. Persistence Layer
|
||||
|
||||
- Remove `reminders` from `BASE_MODEL_CFGS` in `src/app/core/persistence/persistence.const.ts`.
|
||||
- Ensure `Task` persistence (which already exists) captures the new `remindAt` field.
|
||||
|
||||
### 3. Service Layer (`ReminderService`)
|
||||
|
||||
The `ReminderService` will transition from a state-holding service to a facade that interacts with the Task Store.
|
||||
|
||||
- **State Management**: Remove local `_reminders` array, `BehaviorSubject`, and manual persistence calls (`_saveModel`, `loadFromDatabase`).
|
||||
- **Data Source**:
|
||||
- Inject `Store`.
|
||||
- Create a selector `selectTasksWithReminders` in Task Store.
|
||||
- `onRemindersActive$` will subscribe to this selector (mapped to the format expected by the worker).
|
||||
- **Actions**:
|
||||
- `addReminder(taskId, remindAt)`: Dispatch `updateTask({ id: taskId, changes: { remindAt } })`.
|
||||
- `updateReminder(taskId, changes)`: Dispatch `updateTask({ id: taskId, changes: { remindAt: changes.remindAt } })`.
|
||||
- `removeReminder(taskId)`: Dispatch `updateTask({ id: taskId, changes: { remindAt: null } })`.
|
||||
- `snooze(taskId, snoozeTime)`: Dispatch `updateTask` with new calculated time.
|
||||
- **Worker Integration**:
|
||||
- Continue to use `reminder.worker.ts`.
|
||||
- The service will map `Task` objects to the lightweight structure expected by the worker (id, title, remindAt, type='TASK').
|
||||
|
||||
### 4. Migration Strategy
|
||||
|
||||
A migration must run **once** on startup to transfer existing reminder data to tasks.
|
||||
|
||||
**Migration Logic**:
|
||||
|
||||
1. Load the legacy `reminders` state (from `reminders.json` or IndexedDB).
|
||||
2. Load the `task` state.
|
||||
3. Iterate through all legacy reminders:
|
||||
- If `type === 'NOTE'`: Discard (log warning if needed).
|
||||
- If `type === 'TASK'`: Find the corresponding Task by `relatedId`. Update its `remindAt` property with the reminder's `remindAt`.
|
||||
4. Save the updated `task` state.
|
||||
5. Delete/Clear the `reminders` state to prevent future conflicts.
|
||||
|
||||
### 5. UI & Cleanup
|
||||
|
||||
- **Dialogs**: Update `DialogViewTaskReminders` and "Add/Edit Reminder" dialogs to read/write `task.remindAt` instead of looking up a separate reminder object.
|
||||
- **Note UI**: Remove "Add Reminder" options from Note context menus and buttons.
|
||||
- **Code Removal**: Delete unused `reminder.model.ts` (or reduce to Worker interface), remove Note-specific reminder logic.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Backup**: Ensure data is backed up before applying changes.
|
||||
2. **Model Update**: specificy `remindAt` in `TaskCopy` / `Task`.
|
||||
3. **Migration Script**: Implement the logic to merge `reminders` into `tasks`.
|
||||
4. **Service Refactoring**: Rewrite `ReminderService` to use `TaskStore`.
|
||||
5. **UI Adjustments**: Fix compilation errors in components accessing `reminderId`.
|
||||
6. **Cleanup**: Remove dead code and `reminders` persistence config.
|
||||
7. **Verification**: Test adding, snoozing, completing, and removing reminders. Verify sync behavior.
|
||||
|
|
@ -216,7 +216,9 @@ export interface Task {
|
|||
timeSpentOnDay?: { [key: string]: number };
|
||||
doneOn?: number | null;
|
||||
attachments?: any[];
|
||||
/** @deprecated Use remindAt instead */
|
||||
reminderId?: string | null;
|
||||
remindAt?: number | null;
|
||||
repeatCfgId?: string | null;
|
||||
|
||||
// Issue tracking fields (optional)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { Action } from '@ngrx/store';
|
||||
import { isPersistentAction, PersistentAction } from './persistent-action.interface';
|
||||
import { OpType } from './operation.types';
|
||||
|
||||
describe('PersistentAction Interface', () => {
|
||||
describe('isPersistentAction', () => {
|
||||
it('should return true for a valid PersistentAction', () => {
|
||||
const action: PersistentAction = {
|
||||
type: '[Test] Action',
|
||||
meta: {
|
||||
isPersistent: true,
|
||||
entityType: 'TASK',
|
||||
opType: OpType.Create,
|
||||
},
|
||||
};
|
||||
expect(isPersistentAction(action)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an action without meta', () => {
|
||||
const action: Action = {
|
||||
type: '[Test] Simple Action',
|
||||
};
|
||||
expect(isPersistentAction(action)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for an action with meta but isPersistent false', () => {
|
||||
const action = {
|
||||
type: '[Test] Non-persistent Action',
|
||||
meta: {
|
||||
isPersistent: false,
|
||||
entityType: 'TASK',
|
||||
opType: OpType.Create,
|
||||
},
|
||||
};
|
||||
expect(isPersistentAction(action as unknown as Action)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for an action with meta but isPersistent undefined', () => {
|
||||
const action = {
|
||||
type: '[Test] Undefined Persistence Action',
|
||||
meta: {
|
||||
entityType: 'TASK',
|
||||
opType: OpType.Create,
|
||||
},
|
||||
};
|
||||
expect(isPersistentAction(action as unknown as Action)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if action is null or undefined', () => {
|
||||
expect(isPersistentAction(null as any)).toBe(false);
|
||||
expect(isPersistentAction(undefined as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if action is not an object', () => {
|
||||
expect(isPersistentAction('string' as any)).toBe(false);
|
||||
expect(isPersistentAction(123 as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -20,5 +20,5 @@ export interface PersistentAction extends Action {
|
|||
// Helper type guard - only actions with explicit isPersistent: true are persisted
|
||||
export const isPersistentAction = (action: Action): action is PersistentAction => {
|
||||
const a = action as PersistentAction;
|
||||
return !!a.meta && a.meta.isPersistent === true;
|
||||
return !!a && !!a.meta && a.meta.isPersistent === true;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -331,15 +331,9 @@ export class BoardPanelComponent {
|
|||
}
|
||||
}
|
||||
if (panelCfg.scheduledState === BoardPanelCfgScheduledState.NotScheduled) {
|
||||
const task = await this.store
|
||||
.select(selectTaskById, { id: taskId })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
this.store.dispatch(
|
||||
TaskSharedActions.unscheduleTask({
|
||||
id: taskId,
|
||||
reminderId: task.reminderId,
|
||||
isSkipToast: false,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,6 @@ export const updateGlobalConfigSection = createAction(
|
|||
entityType: 'GLOBAL_CONFIG',
|
||||
entityId: configProps.sectionKey, // Use section key as entity ID
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const IssueProviderActions = createActionGroup({
|
|||
entityType: 'ISSUE_PROVIDER',
|
||||
entityId: providerProps.issueProvider.id,
|
||||
opType: OpType.Create,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
|
||||
// Upsert is typically used for sync/import, so no persistence metadata
|
||||
|
|
@ -40,7 +40,7 @@ export const IssueProviderActions = createActionGroup({
|
|||
entityType: 'ISSUE_PROVIDER',
|
||||
entityId: providerProps.issueProvider.id as string,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
|
||||
// Bulk update is typically used for sync/import, so no persistence metadata
|
||||
|
|
@ -54,7 +54,7 @@ export const IssueProviderActions = createActionGroup({
|
|||
entityIds: providerProps.ids,
|
||||
opType: OpType.Move,
|
||||
isBulk: true,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
|
||||
'Delete IssueProvider': (providerProps: { id: string }) => ({
|
||||
|
|
@ -64,7 +64,7 @@ export const IssueProviderActions = createActionGroup({
|
|||
entityType: 'ISSUE_PROVIDER',
|
||||
entityId: providerProps.id,
|
||||
opType: OpType.Delete,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
|
||||
'Delete IssueProviders': (providerProps: { ids: string[] }) => ({
|
||||
|
|
@ -75,7 +75,7 @@ export const IssueProviderActions = createActionGroup({
|
|||
entityIds: providerProps.ids,
|
||||
opType: OpType.Delete,
|
||||
isBulk: true,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
|
||||
// Internal cleanup action - no persistence
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const updateNoteOrder = createAction(
|
|||
entityIds: noteProps.ids,
|
||||
opType: OpType.Move,
|
||||
isBulk: true,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ export const addNote = createAction(
|
|||
entityType: 'NOTE',
|
||||
entityId: noteProps.note.id,
|
||||
opType: OpType.Create,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ export const updateNote = createAction(
|
|||
entityType: 'NOTE',
|
||||
entityId: noteProps.note.id as string,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ export const deleteNote = createAction(
|
|||
entityType: 'NOTE',
|
||||
entityId: noteProps.id,
|
||||
opType: OpType.Delete,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -71,6 +71,6 @@ export const moveNoteToOtherProject = createAction(
|
|||
entityType: 'NOTE',
|
||||
entityId: noteProps.note.id,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import { PlannerActions } from '../store/planner.actions';
|
|||
import { getDbDateStr } from '../../../util/get-db-date-str';
|
||||
import { LocaleDatePipe } from 'src/app/ui/pipes/locale-date.pipe';
|
||||
import { SnackService } from '../../../core/snack/snack.service';
|
||||
import { removeReminderFromTask } from '../../tasks/store/task.actions';
|
||||
import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions';
|
||||
import { truncate } from '../../../util/truncate';
|
||||
import { TASK_REMINDER_OPTIONS } from './task-reminder-options.const';
|
||||
|
|
@ -278,7 +277,6 @@ export class DialogScheduleTaskComponent implements AfterViewInit {
|
|||
this._store.dispatch(
|
||||
TaskSharedActions.unscheduleTask({
|
||||
id: this.data.task.id,
|
||||
reminderId: this.data.task.reminderId,
|
||||
}),
|
||||
);
|
||||
} else if (this.plannedDayForTask === getDbDateStr()) {
|
||||
|
|
@ -286,7 +284,6 @@ export class DialogScheduleTaskComponent implements AfterViewInit {
|
|||
this._store.dispatch(
|
||||
TaskSharedActions.unscheduleTask({
|
||||
id: this.data.task.id,
|
||||
reminderId: this.data.task.reminderId,
|
||||
isSkipToast: true,
|
||||
}),
|
||||
);
|
||||
|
|
@ -300,7 +297,6 @@ export class DialogScheduleTaskComponent implements AfterViewInit {
|
|||
this._store.dispatch(
|
||||
TaskSharedActions.unscheduleTask({
|
||||
id: this.data.task.id,
|
||||
reminderId: this.data.task.reminderId,
|
||||
isSkipToast: true,
|
||||
}),
|
||||
);
|
||||
|
|
@ -392,11 +388,13 @@ export class DialogScheduleTaskComponent implements AfterViewInit {
|
|||
typeof this.data.task.reminderId === 'string'
|
||||
) {
|
||||
this._store.dispatch(
|
||||
removeReminderFromTask({
|
||||
id: this.data.task.id,
|
||||
reminderId: this.data.task.reminderId,
|
||||
isSkipToast: true,
|
||||
isLeaveDueTime: true,
|
||||
TaskSharedActions.updateTask({
|
||||
task: {
|
||||
id: this.data.task.id,
|
||||
changes: {
|
||||
remindAt: undefined,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const PlannerActions = createActionGroup({
|
|||
entityType: 'PLANNER',
|
||||
entityId: plannerProps.day,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
|
||||
// Internal cleanup action - no persistence
|
||||
|
|
@ -41,7 +41,7 @@ export const PlannerActions = createActionGroup({
|
|||
entityType: 'PLANNER',
|
||||
entityId: plannerProps.task.id,
|
||||
opType: OpType.Move,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
|
||||
'Move In List': (plannerProps: {
|
||||
|
|
@ -55,7 +55,7 @@ export const PlannerActions = createActionGroup({
|
|||
entityType: 'PLANNER',
|
||||
entityId: plannerProps.targetDay,
|
||||
opType: OpType.Move,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
|
||||
'Move Before Task': (plannerProps: { fromTask: TaskCopy; toTaskId: string }) => ({
|
||||
|
|
@ -65,7 +65,7 @@ export const PlannerActions = createActionGroup({
|
|||
entityType: 'PLANNER',
|
||||
entityId: plannerProps.fromTask.id,
|
||||
opType: OpType.Move,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
|
||||
'Plan Task for Day': (plannerProps: {
|
||||
|
|
@ -80,7 +80,7 @@ export const PlannerActions = createActionGroup({
|
|||
entityType: 'PLANNER',
|
||||
entityId: plannerProps.task.id,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
|
||||
// UI state action - no persistence
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const addProject = createAction(
|
|||
entityType: 'PROJECT',
|
||||
entityId: projectProps.project.id,
|
||||
opType: OpType.Create,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ export const updateProject = createAction(
|
|||
entityType: 'PROJECT',
|
||||
entityId: projectProps.project.id as string,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ export const updateProjectAdvancedCfg = createAction(
|
|||
entityType: 'PROJECT',
|
||||
entityId: projectProps.projectId,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ export const updateProjectOrder = createAction(
|
|||
entityIds: projectProps.ids,
|
||||
opType: OpType.Move,
|
||||
isBulk: true,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ export const archiveProject = createAction(
|
|||
entityType: 'PROJECT',
|
||||
entityId: projectProps.id,
|
||||
opType: OpType.Update, // Archiving is an update
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ export const unarchiveProject = createAction(
|
|||
entityType: 'PROJECT',
|
||||
entityId: projectProps.id,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ export const toggleHideFromMenu = createAction(
|
|||
entityType: 'PROJECT',
|
||||
entityId: projectProps.id,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -133,7 +133,7 @@ export const moveProjectTaskToBacklogListAuto = createAction(
|
|||
entityType: 'TASK',
|
||||
entityId: taskProps.taskId,
|
||||
opType: OpType.Move,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -146,7 +146,7 @@ export const moveProjectTaskToRegularListAuto = createAction(
|
|||
entityType: 'TASK',
|
||||
entityId: taskProps.taskId,
|
||||
opType: OpType.Move,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ export const moveProjectTaskUpInBacklogList = createAction(
|
|||
entityType: 'TASK',
|
||||
entityId: taskProps.taskId,
|
||||
opType: OpType.Move,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -180,7 +180,7 @@ export const moveProjectTaskDownInBacklogList = createAction(
|
|||
entityType: 'TASK',
|
||||
entityId: taskProps.taskId,
|
||||
opType: OpType.Move,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -197,7 +197,7 @@ export const moveProjectTaskToTopInBacklogList = createAction(
|
|||
entityType: 'TASK',
|
||||
entityId: taskProps.taskId,
|
||||
opType: OpType.Move,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -214,7 +214,7 @@ export const moveProjectTaskToBottomInBacklogList = createAction(
|
|||
entityType: 'TASK',
|
||||
entityId: taskProps.taskId,
|
||||
opType: OpType.Move,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -227,7 +227,7 @@ export const moveProjectTaskInBacklogList = createAction(
|
|||
entityType: 'TASK',
|
||||
entityId: taskProps.taskId,
|
||||
opType: OpType.Move,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -240,7 +240,7 @@ export const moveProjectTaskToBacklogList = createAction(
|
|||
entityType: 'TASK',
|
||||
entityId: taskProps.taskId,
|
||||
opType: OpType.Move,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -259,7 +259,7 @@ export const moveProjectTaskToRegularList = createAction(
|
|||
entityType: 'TASK',
|
||||
entityId: taskProps.taskId,
|
||||
opType: OpType.Move,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -272,6 +272,6 @@ export const moveAllProjectBacklogTasksToRegularList = createAction(
|
|||
entityType: 'PROJECT',
|
||||
entityId: taskProps.projectId,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,35 +1,36 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { RecurringConfig, Reminder, ReminderCopy, ReminderType } from './reminder.model';
|
||||
import { SnackService } from '../../core/snack/snack.service';
|
||||
import { BehaviorSubject, Observable, ReplaySubject, Subject } from 'rxjs';
|
||||
import { dirtyDeepCopy } from '../../util/dirtyDeepCopy';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { ImexViewService } from '../../imex/imex-meta/imex-view.service';
|
||||
import { TaskService } from '../tasks/task.service';
|
||||
import { Task } from '../tasks/task.model';
|
||||
import { NoteService } from '../note/note.service';
|
||||
import { T } from '../../t.const';
|
||||
import { filter, first, skipUntil } from 'rxjs/operators';
|
||||
import { devError } from '../../util/dev-error';
|
||||
import { Note } from '../note/note.model';
|
||||
import { filter, map, skipUntil } from 'rxjs/operators';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { PfapiService } from '../../pfapi/pfapi.service';
|
||||
import { Log } from '../../core/log';
|
||||
import { GlobalConfigService } from '../config/global-config.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { selectAllTasksWithReminder } from '../tasks/store/task.selectors';
|
||||
import { TaskWithReminder, TaskWithReminderData } from '../tasks/task.model';
|
||||
|
||||
interface WorkerReminder {
|
||||
id: string;
|
||||
remindAt: number;
|
||||
title: string;
|
||||
type: 'TASK';
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ReminderService {
|
||||
private readonly _pfapiService = inject(PfapiService);
|
||||
private readonly _snackService = inject(SnackService);
|
||||
private readonly _taskService = inject(TaskService);
|
||||
private readonly _noteService = inject(NoteService);
|
||||
private readonly _imexMetaService = inject(ImexViewService);
|
||||
private readonly _globalConfigService = inject(GlobalConfigService);
|
||||
private readonly _store = inject(Store);
|
||||
|
||||
private _onRemindersActive$: Subject<Reminder[]> = new Subject<Reminder[]>();
|
||||
onRemindersActive$: Observable<Reminder[]> = this._onRemindersActive$.pipe(
|
||||
private _onRemindersActive$: Subject<TaskWithReminderData[]> = new Subject<
|
||||
TaskWithReminderData[]
|
||||
>();
|
||||
onRemindersActive$: Observable<TaskWithReminderData[]> = this._onRemindersActive$.pipe(
|
||||
skipUntil(
|
||||
this._imexMetaService.isDataImportInProgress$.pipe(
|
||||
filter((isInProgress) => !isInProgress),
|
||||
|
|
@ -37,26 +38,10 @@ export class ReminderService {
|
|||
),
|
||||
);
|
||||
|
||||
private _reminders$: ReplaySubject<Reminder[]> = new ReplaySubject(1);
|
||||
reminders$: Observable<Reminder[]> = this._reminders$.asObservable();
|
||||
|
||||
private _onReloadModel$: Subject<Reminder[]> = new Subject();
|
||||
onReloadModel$: Observable<Reminder[]> = this._onReloadModel$.asObservable();
|
||||
|
||||
private _isRemindersLoaded$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(
|
||||
false,
|
||||
);
|
||||
|
||||
private _w: Worker;
|
||||
private _reminders: Reminder[] = [];
|
||||
|
||||
constructor() {
|
||||
// this._triggerPauseAfterUpdate$.subscribe((v) => Log.log('_triggerPauseAfterUpdate$', v));
|
||||
// this._pauseAfterUpdate$.subscribe((v) => Log.log('_pauseAfterUpdate$', v));
|
||||
// this._onRemindersActive$.subscribe((v) => Log.log('_onRemindersActive$', v));
|
||||
// this.onRemindersActive$.subscribe((v) => Log.log('onRemindersActive$', v));
|
||||
|
||||
if (typeof (Worker as any) === 'undefined') {
|
||||
if (typeof (Worker as unknown) === 'undefined') {
|
||||
throw new Error('No service workers supported :(');
|
||||
}
|
||||
|
||||
|
|
@ -66,202 +51,56 @@ export class ReminderService {
|
|||
});
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
init(): void {
|
||||
this._w.addEventListener('message', this._onReminderActivated.bind(this));
|
||||
this._w.addEventListener('error', this._handleError.bind(this));
|
||||
await this.reloadFromDatabase();
|
||||
this._isRemindersLoaded$.next(true);
|
||||
}
|
||||
|
||||
async reloadFromDatabase(): Promise<void> {
|
||||
const fromDb = await this._loadFromDatabase();
|
||||
if (!fromDb || !Array.isArray(fromDb)) {
|
||||
this._saveModel([]);
|
||||
}
|
||||
this._reminders = await this._loadFromDatabase();
|
||||
if (!Array.isArray(this._reminders)) {
|
||||
Log.log(this._reminders);
|
||||
devError('Something went wrong with the reminders');
|
||||
this._reminders = [];
|
||||
}
|
||||
|
||||
this._updateRemindersInWorker(this._reminders);
|
||||
this._onReloadModel$.next(this._reminders);
|
||||
this._reminders$.next(this._reminders);
|
||||
if (environment.production) {
|
||||
Log.log('loaded reminders from database', this._reminders);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO maybe refactor to observable, because models can differ to sync value for yet unknown reasons
|
||||
getById(reminderId: string): ReminderCopy | null {
|
||||
const _foundReminder =
|
||||
this._reminders && this._reminders.find((reminder) => reminder.id === reminderId);
|
||||
return !!_foundReminder ? dirtyDeepCopy<ReminderCopy>(_foundReminder) : null;
|
||||
}
|
||||
|
||||
getByRelatedId(relatedId: string): ReminderCopy | null {
|
||||
const _foundReminder =
|
||||
this._reminders &&
|
||||
this._reminders.find((reminder) => reminder.relatedId === relatedId);
|
||||
return !!_foundReminder ? dirtyDeepCopy<ReminderCopy>(_foundReminder) : null;
|
||||
}
|
||||
|
||||
addReminder(
|
||||
type: ReminderType,
|
||||
relatedId: string,
|
||||
title: string,
|
||||
remindAt: number,
|
||||
recurringConfig?: RecurringConfig,
|
||||
isWaitForReady: boolean = false,
|
||||
): string {
|
||||
// make sure that there is always only a single reminder with a particular relatedId as there might be race conditions
|
||||
this.removeReminderByRelatedIdIfSet(relatedId);
|
||||
|
||||
const id = nanoid();
|
||||
const existingInstanceForEntry = this.getByRelatedId(relatedId);
|
||||
if (existingInstanceForEntry) {
|
||||
devError('A reminder for this ' + type + ' already exists');
|
||||
this.updateReminder(existingInstanceForEntry.id, {
|
||||
relatedId,
|
||||
title,
|
||||
remindAt,
|
||||
type,
|
||||
recurringConfig,
|
||||
// Subscribe to tasks with reminders and update worker whenever they change
|
||||
this._store
|
||||
.select(selectAllTasksWithReminder)
|
||||
.pipe(map((tasks) => this._mapTasksToWorkerReminders(tasks)))
|
||||
.subscribe((reminders) => {
|
||||
this._updateRemindersInWorker(reminders);
|
||||
if (!environment.production) {
|
||||
Log.log('Updated reminders in worker', reminders);
|
||||
}
|
||||
});
|
||||
return existingInstanceForEntry.id;
|
||||
} else {
|
||||
// TODO find out why we need to do this
|
||||
this._reminders = dirtyDeepCopy(this._reminders);
|
||||
this._reminders.push({
|
||||
id,
|
||||
relatedId,
|
||||
title,
|
||||
remindAt,
|
||||
type,
|
||||
recurringConfig,
|
||||
});
|
||||
this._saveModel(this._reminders, isWaitForReady);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
snooze(reminderId: string, snoozeTime: number): void {
|
||||
const remindAt = new Date().getTime() + snoozeTime;
|
||||
this.updateReminder(reminderId, { remindAt });
|
||||
private _mapTasksToWorkerReminders(tasks: TaskWithReminder[]): WorkerReminder[] {
|
||||
return tasks.map((task) => ({
|
||||
id: task.id,
|
||||
remindAt: task.remindAt,
|
||||
title: task.title,
|
||||
type: 'TASK' as const,
|
||||
}));
|
||||
}
|
||||
|
||||
updateReminder(reminderId: string, reminderChanges: Partial<Reminder>): void {
|
||||
const i = this._reminders.findIndex((reminder) => reminder.id === reminderId);
|
||||
if (i > -1) {
|
||||
// TODO find out why we need to do this
|
||||
this._reminders = dirtyDeepCopy(this._reminders);
|
||||
this._reminders[i] = Object.assign({}, this._reminders[i], reminderChanges);
|
||||
}
|
||||
this._saveModel(this._reminders);
|
||||
}
|
||||
|
||||
removeReminder(reminderIdToRemove: string): void {
|
||||
const i = this._reminders.findIndex((reminder) => reminder.id === reminderIdToRemove);
|
||||
|
||||
if (i > -1) {
|
||||
// TODO find out why we need to do this
|
||||
this._reminders = dirtyDeepCopy(this._reminders);
|
||||
this._reminders.splice(i, 1);
|
||||
this._saveModel(this._reminders);
|
||||
} else {
|
||||
// throw new Error('Unable to find reminder with id ' + reminderIdToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
removeReminderByRelatedIdIfSet(relatedId: string): void {
|
||||
const reminder = this._reminders.find(
|
||||
(reminderIN) => reminderIN.relatedId === relatedId,
|
||||
);
|
||||
if (reminder) {
|
||||
this.removeReminder(reminder.id);
|
||||
}
|
||||
}
|
||||
|
||||
removeRemindersByRelatedIds(relatedIds: string[]): void {
|
||||
const reminders = this._reminders.filter((reminderIN) =>
|
||||
relatedIds.includes(reminderIN.relatedId),
|
||||
);
|
||||
if (reminders && reminders.length) {
|
||||
reminders.forEach((reminder) => {
|
||||
this.removeReminder(reminder.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _onReminderActivated(msg: MessageEvent): Promise<void> {
|
||||
const reminders = msg.data as Reminder[];
|
||||
Log.log(`ReminderService: Worker activated ${reminders.length} reminder(s)`);
|
||||
private _onReminderActivated(msg: MessageEvent): void {
|
||||
const reminders = msg.data as WorkerReminder[];
|
||||
Log.log(`ReminderService: Worker activated ${reminders.length} reminder(s)`);
|
||||
|
||||
if (this._globalConfigService.cfg()?.reminder?.disableReminders) {
|
||||
Log.log('ReminderService: reminders are disabled, not sending to UI');
|
||||
return;
|
||||
}
|
||||
|
||||
const remindersWithData: Reminder[] = (await Promise.all(
|
||||
reminders.map(async (reminder) => {
|
||||
const relatedModel = await this._getRelatedDataForReminder(reminder);
|
||||
// Log.log('RelatedModel for Reminder', relatedModel);
|
||||
// only show when not currently syncing and related model still exists
|
||||
if (!relatedModel) {
|
||||
Log.warn(
|
||||
`ReminderService: No related data found for reminder ${reminder.id} (${reminder.type}: ${reminder.relatedId}), removing...`,
|
||||
);
|
||||
this.removeReminder(reminder.id);
|
||||
return null;
|
||||
} else {
|
||||
// Check if task is already done (defensive check)
|
||||
if (reminder.type === 'TASK' && (relatedModel as Task).isDone) {
|
||||
Log.warn(
|
||||
`ReminderService: Task ${relatedModel.id} is already done but reminder ${reminder.id} still exists, removing...`,
|
||||
);
|
||||
this.removeReminder(reminder.id);
|
||||
return null;
|
||||
}
|
||||
return reminder;
|
||||
}
|
||||
}),
|
||||
)) as Reminder[];
|
||||
const finalReminders = remindersWithData.filter((reminder) => !!reminder);
|
||||
// Map worker reminders back to TaskWithReminderData format
|
||||
const taskReminders: TaskWithReminderData[] = reminders.map((r) => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
reminderData: { remindAt: r.remindAt },
|
||||
// These fields will be populated by the component that consumes this
|
||||
// by looking up the full task from the store
|
||||
})) as TaskWithReminderData[];
|
||||
|
||||
Log.log(`ReminderService: ${finalReminders.length} valid reminder(s) to show`);
|
||||
if (finalReminders.length > 0) {
|
||||
this._onRemindersActive$.next(finalReminders);
|
||||
Log.log(`ReminderService: ${taskReminders.length} valid reminder(s) to show`);
|
||||
if (taskReminders.length > 0) {
|
||||
this._onRemindersActive$.next(taskReminders);
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadFromDatabase(): Promise<Reminder[]> {
|
||||
return (await this._pfapiService.m.reminders.load()) || [];
|
||||
}
|
||||
|
||||
private async _saveModel(
|
||||
reminders: Reminder[],
|
||||
isWaitForReady: boolean = false,
|
||||
): Promise<void> {
|
||||
if (isWaitForReady) {
|
||||
await this._isRemindersLoaded$
|
||||
.pipe(
|
||||
filter((v) => !!v),
|
||||
first(),
|
||||
)
|
||||
.toPromise();
|
||||
} else if (!this._isRemindersLoaded$.getValue()) {
|
||||
throw new Error('Reminders not loaded initially when trying to save model');
|
||||
}
|
||||
Log.log('saveReminders', reminders);
|
||||
await this._pfapiService.m.reminders.save(reminders, {
|
||||
isUpdateRevAndLastUpdate: true,
|
||||
});
|
||||
this._updateRemindersInWorker(this._reminders);
|
||||
this._reminders$.next(this._reminders);
|
||||
}
|
||||
|
||||
private _updateRemindersInWorker(reminders: Reminder[]): void {
|
||||
private _updateRemindersInWorker(reminders: WorkerReminder[]): void {
|
||||
this._w.postMessage(reminders);
|
||||
}
|
||||
|
||||
|
|
@ -269,16 +108,4 @@ export class ReminderService {
|
|||
Log.err(err);
|
||||
this._snackService.open({ type: 'ERROR', msg: T.F.REMINDER.S_REMINDER_ERR });
|
||||
}
|
||||
|
||||
private async _getRelatedDataForReminder(reminder: Reminder): Promise<Task | Note> {
|
||||
switch (reminder.type) {
|
||||
case 'NOTE':
|
||||
return await this._noteService.getByIdOnce$(reminder.relatedId).toPromise();
|
||||
case 'TASK':
|
||||
// NOTE: remember we don't want archive tasks to pop up here
|
||||
return await this._taskService.getByIdOnce$(reminder.relatedId).toPromise();
|
||||
}
|
||||
|
||||
throw new Error('Cannot get related model for reminder');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createSortedBlockerBlocks } from './create-sorted-blocker-blocks';
|
||||
import { TaskReminderOptionId, TaskWithReminder } from '../../tasks/task.model';
|
||||
import { TaskReminderOptionId } from '../../tasks/task.model';
|
||||
import { getDateTimeFromClockString } from '../../../util/get-date-time-from-clock-string';
|
||||
import {
|
||||
DEFAULT_TASK_REPEAT_CFG,
|
||||
|
|
@ -108,7 +108,7 @@ const generateBlockedBlocks = (
|
|||
|
||||
describe('createBlockerBlocks()', () => {
|
||||
it('should merge into single block if all overlapping', () => {
|
||||
const fakeTasks: TaskWithReminder[] = [
|
||||
const fakeTasks: any[] = [
|
||||
{
|
||||
id: 'S1',
|
||||
subTaskIds: [],
|
||||
|
|
@ -144,7 +144,7 @@ describe('createBlockerBlocks()', () => {
|
|||
});
|
||||
|
||||
it('should merge into multiple blocks if not overlapping', () => {
|
||||
const fakeTasks: TaskWithReminder[] = [
|
||||
const fakeTasks: any[] = [
|
||||
{
|
||||
id: 'S1',
|
||||
subTaskIds: [],
|
||||
|
|
@ -201,7 +201,7 @@ describe('createBlockerBlocks()', () => {
|
|||
});
|
||||
|
||||
it('should work for advanced scenario', () => {
|
||||
const fakeTasks: TaskWithReminder[] = [
|
||||
const fakeTasks: any[] = [
|
||||
{
|
||||
id: 'S1',
|
||||
subTaskIds: [],
|
||||
|
|
@ -237,7 +237,7 @@ describe('createBlockerBlocks()', () => {
|
|||
});
|
||||
|
||||
it('should merge multiple times', () => {
|
||||
const fakeTasks: TaskWithReminder[] = [
|
||||
const fakeTasks: any[] = [
|
||||
{
|
||||
...BASE_REMINDER_TASK('16:00', 'no duration'),
|
||||
timeEstimate: 0,
|
||||
|
|
@ -310,7 +310,7 @@ describe('createBlockerBlocks()', () => {
|
|||
});
|
||||
|
||||
it('should work with far future entries', () => {
|
||||
const fakeTasks: TaskWithReminder[] = [
|
||||
const fakeTasks: any[] = [
|
||||
{
|
||||
...BASE_REMINDER_TASK('16:00', 'no duration'),
|
||||
timeEstimate: 0,
|
||||
|
|
@ -352,7 +352,7 @@ describe('createBlockerBlocks()', () => {
|
|||
});
|
||||
|
||||
it('should work for advanced scenario', () => {
|
||||
const fakeTasks: TaskWithReminder[] = [
|
||||
const fakeTasks: any[] = [
|
||||
{
|
||||
id: '0LtuSnH8s',
|
||||
projectId: null,
|
||||
|
|
@ -1001,7 +1001,7 @@ describe('createBlockerBlocks()', () => {
|
|||
],
|
||||
},
|
||||
];
|
||||
const fakeTasks: TaskWithReminder[] = [
|
||||
const fakeTasks: any[] = [
|
||||
{
|
||||
id: 'S1',
|
||||
timeSpent: 0,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const addSimpleCounter = createAction(
|
|||
entityType: 'SIMPLE_COUNTER',
|
||||
entityId: counterProps.simpleCounter.id,
|
||||
opType: OpType.Create,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ export const updateSimpleCounter = createAction(
|
|||
entityType: 'SIMPLE_COUNTER',
|
||||
entityId: counterProps.simpleCounter.id as string,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ export const deleteSimpleCounter = createAction(
|
|||
entityType: 'SIMPLE_COUNTER',
|
||||
entityId: counterProps.id,
|
||||
opType: OpType.Delete,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ export const deleteSimpleCounters = createAction(
|
|||
entityIds: counterProps.ids,
|
||||
opType: OpType.Delete,
|
||||
isBulk: true,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ export const setSimpleCounterCounterToday = createAction(
|
|||
entityType: 'SIMPLE_COUNTER',
|
||||
entityId: counterProps.id,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ export const setSimpleCounterCounterForDate = createAction(
|
|||
entityType: 'SIMPLE_COUNTER',
|
||||
entityId: counterProps.id,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ export const increaseSimpleCounterCounterToday = createAction(
|
|||
entityType: 'SIMPLE_COUNTER',
|
||||
entityId: counterProps.id,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -117,7 +117,7 @@ export const decreaseSimpleCounterCounterToday = createAction(
|
|||
entityType: 'SIMPLE_COUNTER',
|
||||
entityId: counterProps.id,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ export const toggleSimpleCounterCounter = createAction(
|
|||
entityType: 'SIMPLE_COUNTER',
|
||||
entityId: counterProps.id,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ export const setSimpleCounterCounterOff = createAction(
|
|||
entityType: 'SIMPLE_COUNTER',
|
||||
entityId: counterProps.id,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -161,6 +161,6 @@ export const setSimpleCounterCounterOn = createAction(
|
|||
entityType: 'SIMPLE_COUNTER',
|
||||
entityId: counterProps.id,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createAction, props } from '@ngrx/store';
|
||||
import { createAction } from '@ngrx/store';
|
||||
import { Update } from '@ngrx/entity';
|
||||
import { Tag } from '../tag.model';
|
||||
import { WorkContextAdvancedCfgKey } from '../../work-context/work-context.model';
|
||||
|
|
@ -12,7 +12,7 @@ export const addTag = createAction('[Tag] Add Tag', (tagProps: { tag: Tag }) =>
|
|||
entityType: 'TAG',
|
||||
entityId: tagProps.tag.id,
|
||||
opType: OpType.Create,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}));
|
||||
|
||||
export const updateTag = createAction(
|
||||
|
|
@ -24,12 +24,10 @@ export const updateTag = createAction(
|
|||
entityType: 'TAG',
|
||||
entityId: tagProps.tag.id as string,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
export const upsertTag = createAction('[Tag] Upsert Tag', props<{ tag: Tag }>());
|
||||
|
||||
export const deleteTag = createAction('[Tag] Delete Tag', (tagProps: { id: string }) => ({
|
||||
...tagProps,
|
||||
meta: {
|
||||
|
|
@ -37,7 +35,7 @@ export const deleteTag = createAction('[Tag] Delete Tag', (tagProps: { id: strin
|
|||
entityType: 'TAG',
|
||||
entityId: tagProps.id,
|
||||
opType: OpType.Delete,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}));
|
||||
|
||||
export const deleteTags = createAction(
|
||||
|
|
@ -50,7 +48,7 @@ export const deleteTags = createAction(
|
|||
entityIds: tagProps.ids,
|
||||
opType: OpType.Delete,
|
||||
isBulk: true,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -64,7 +62,7 @@ export const updateTagOrder = createAction(
|
|||
entityIds: tagProps.ids,
|
||||
opType: OpType.Move,
|
||||
isBulk: true,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -77,6 +75,6 @@ export const updateAdvancedConfigForTag = createAction(
|
|||
entityType: 'TAG',
|
||||
entityId: tagProps.tagId,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import {
|
|||
updateAdvancedConfigForTag,
|
||||
updateTag,
|
||||
updateTagOrder,
|
||||
upsertTag,
|
||||
} from './tag.actions';
|
||||
import { PlannerActions } from '../../planner/store/planner.actions';
|
||||
import { getDbDateStr } from '../../../util/get-db-date-str';
|
||||
|
|
@ -298,8 +297,6 @@ export const tagReducer = createReducer<TagState>(
|
|||
|
||||
on(updateTag, (state: TagState, { tag }) => tagAdapter.updateOne(tag, state)),
|
||||
|
||||
on(upsertTag, (state: TagState, { tag }) => tagAdapter.upsertOne(tag, state)),
|
||||
|
||||
on(deleteTag, (state: TagState, { id }) => tagAdapter.removeOne(id, state)),
|
||||
|
||||
on(deleteTags, (state: TagState, { ids }) => tagAdapter.removeMany(ids, state)),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
deleteTags,
|
||||
updateTag,
|
||||
updateTagOrder,
|
||||
upsertTag,
|
||||
} from './store/tag.actions';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
|
@ -82,10 +81,6 @@ export class TagService {
|
|||
this._store$.dispatch(updateTag({ tag: { id, changes } }));
|
||||
}
|
||||
|
||||
upsertTag(tag: Tag): void {
|
||||
this._store$.dispatch(upsertTag({ tag }));
|
||||
}
|
||||
|
||||
createTagObject(tag: Partial<Tag>): Tag {
|
||||
const id = tag.id || nanoid();
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import {
|
|||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
} from '@angular/material/dialog';
|
||||
import { Reminder } from '../../reminder/reminder.model';
|
||||
import { Task, TaskWithReminderData } from '../task.model';
|
||||
import { TaskService } from '../task.service';
|
||||
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
|
||||
|
|
@ -71,23 +70,25 @@ export class DialogViewTaskRemindersComponent implements OnDestroy {
|
|||
private _store = inject(Store);
|
||||
private _reminderService = inject(ReminderService);
|
||||
data = inject<{
|
||||
reminders: Reminder[];
|
||||
reminders: TaskWithReminderData[];
|
||||
}>(MAT_DIALOG_DATA);
|
||||
|
||||
T: typeof T = T;
|
||||
isDisableControls: boolean = false;
|
||||
reminders$: BehaviorSubject<Reminder[]> = new BehaviorSubject(this.data.reminders);
|
||||
tasks$: Observable<TaskWithReminderData[]> = this.reminders$.pipe(
|
||||
switchMap((reminders) =>
|
||||
this._taskService.getByIdsLive$(reminders.map((r) => r.relatedId)).pipe(
|
||||
taskIds$: BehaviorSubject<string[]> = new BehaviorSubject(
|
||||
this.data.reminders.map((r) => r.id),
|
||||
);
|
||||
tasks$: Observable<TaskWithReminderData[]> = this.taskIds$.pipe(
|
||||
switchMap((taskIds) =>
|
||||
this._taskService.getByIdsLive$(taskIds).pipe(
|
||||
first(),
|
||||
map((tasks: Task[]) =>
|
||||
tasks
|
||||
.filter((task) => !!task)
|
||||
.filter((task) => !!task && typeof task.remindAt === 'number')
|
||||
.map(
|
||||
(task): TaskWithReminderData => ({
|
||||
...task,
|
||||
reminderData: reminders.find((r) => r.relatedId === task.id) as Reminder,
|
||||
reminderData: { remindAt: task.remindAt as number },
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
|
@ -114,15 +115,9 @@ export class DialogViewTaskRemindersComponent implements OnDestroy {
|
|||
private _subs: Subscription = new Subscription();
|
||||
|
||||
constructor() {
|
||||
// this._matDialogRef.disableClose = true;
|
||||
this._subs.add(
|
||||
this._reminderService.onReloadModel$.subscribe(() => {
|
||||
this._close();
|
||||
}),
|
||||
);
|
||||
this._subs.add(
|
||||
this._reminderService.onRemindersActive$.subscribe((reminders) => {
|
||||
this.reminders$.next(reminders);
|
||||
this.taskIds$.next(reminders.map((r) => r.id));
|
||||
}),
|
||||
);
|
||||
this._subs.add(
|
||||
|
|
@ -143,42 +138,41 @@ export class DialogViewTaskRemindersComponent implements OnDestroy {
|
|||
},
|
||||
}),
|
||||
);
|
||||
if (task.reminderId) {
|
||||
this._removeReminderFromList(task.reminderId as string);
|
||||
}
|
||||
this._removeTaskFromList(task.id);
|
||||
}
|
||||
|
||||
dismiss(task: TaskWithReminderData): void {
|
||||
// const now = Date.now();
|
||||
if (task.projectId || task.parentId || task.tagIds.length > 0) {
|
||||
this._store.dispatch(
|
||||
TaskSharedActions.unscheduleTask({
|
||||
id: task.id,
|
||||
reminderId: task.reminderId as string,
|
||||
}),
|
||||
);
|
||||
this._removeReminderFromList(task.reminderId as string);
|
||||
this._removeTaskFromList(task.id);
|
||||
}
|
||||
}
|
||||
|
||||
dismissReminderOnly(task: TaskWithReminderData): void {
|
||||
if (task.reminderId) {
|
||||
this._store.dispatch(
|
||||
TaskSharedActions.dismissReminderOnly({
|
||||
id: task.id,
|
||||
reminderId: task.reminderId as string,
|
||||
}),
|
||||
);
|
||||
this._removeReminderFromList(task.reminderId as string);
|
||||
}
|
||||
this._store.dispatch(
|
||||
TaskSharedActions.dismissReminderOnly({
|
||||
id: task.id,
|
||||
}),
|
||||
);
|
||||
this._removeTaskFromList(task.id);
|
||||
}
|
||||
|
||||
snooze(task: TaskWithReminderData, snoozeInMinutes: number): void {
|
||||
this._reminderService.updateReminder(task.reminderData.id, {
|
||||
// prettier-ignore
|
||||
remindAt: Date.now() + (snoozeInMinutes * MINUTES_TO_MILLISECONDS),
|
||||
});
|
||||
this._removeReminderFromList(task.reminderId as string);
|
||||
const snoozeMs = snoozeInMinutes * MINUTES_TO_MILLISECONDS;
|
||||
const newRemindAt = Date.now() + snoozeMs;
|
||||
this._store.dispatch(
|
||||
TaskSharedActions.reScheduleTaskWithTime({
|
||||
task,
|
||||
dueWithTime: task.dueWithTime || newRemindAt,
|
||||
remindAt: newRemindAt,
|
||||
isMoveToBacklog: false,
|
||||
}),
|
||||
);
|
||||
this._removeTaskFromList(task.id);
|
||||
}
|
||||
|
||||
planForTomorrow(task: TaskWithReminderData): void {
|
||||
|
|
@ -189,7 +183,7 @@ export class DialogViewTaskRemindersComponent implements OnDestroy {
|
|||
isShowSnack: true,
|
||||
}),
|
||||
);
|
||||
this._removeReminderFromList(task.reminderId as string);
|
||||
this._removeTaskFromList(task.id);
|
||||
}
|
||||
|
||||
editReminder(task: TaskWithReminderData, isCloseAfter: boolean = false): void {
|
||||
|
|
@ -202,7 +196,7 @@ export class DialogViewTaskRemindersComponent implements OnDestroy {
|
|||
.afterClosed()
|
||||
.subscribe((wasEdited) => {
|
||||
if (wasEdited) {
|
||||
this._removeReminderFromList(task.reminderId as string);
|
||||
this._removeTaskFromList(task.id);
|
||||
}
|
||||
if (isCloseAfter) {
|
||||
this._close();
|
||||
|
|
@ -219,13 +213,12 @@ export class DialogViewTaskRemindersComponent implements OnDestroy {
|
|||
// ------------
|
||||
snoozeAll(snoozeInMinutes: number): void {
|
||||
this._prepareForBulkAction();
|
||||
this.reminders$.getValue().forEach((reminder) => {
|
||||
this._reminderService.updateReminder(reminder.id, {
|
||||
// prettier-ignore
|
||||
remindAt: Date.now() + (snoozeInMinutes * MINUTES_TO_MILLISECONDS),
|
||||
});
|
||||
});
|
||||
this._finalizeBulkAction();
|
||||
this._subs.add(
|
||||
this.tasks$.pipe(first()).subscribe((tasks) => {
|
||||
tasks.forEach((task) => this.snooze(task, snoozeInMinutes));
|
||||
this._finalizeBulkAction();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
rescheduleAllUntilTomorrow(): void {
|
||||
|
|
@ -251,7 +244,7 @@ export class DialogViewTaskRemindersComponent implements OnDestroy {
|
|||
|
||||
async addAllToToday(): Promise<void> {
|
||||
this._prepareForBulkAction();
|
||||
const selectedTasks = await this._getTasksFromReminderList();
|
||||
const selectedTasks = await this._getTasksFromList();
|
||||
|
||||
this._store.dispatch(
|
||||
TaskSharedActions.planTasksForToday({
|
||||
|
|
@ -268,7 +261,7 @@ export class DialogViewTaskRemindersComponent implements OnDestroy {
|
|||
|
||||
async dismissAll(): Promise<void> {
|
||||
this._prepareForBulkAction();
|
||||
const tasks = await this._getTasksFromReminderList();
|
||||
const tasks = await this._getTasksFromList();
|
||||
tasks.forEach((task) => {
|
||||
if (task.projectId || task.parentId || task.tagIds.length > 0) {
|
||||
this.dismiss(task);
|
||||
|
|
@ -279,7 +272,7 @@ export class DialogViewTaskRemindersComponent implements OnDestroy {
|
|||
|
||||
async dismissAllRemindersOnly(): Promise<void> {
|
||||
this._prepareForBulkAction();
|
||||
const tasks = await this._getTasksFromReminderList();
|
||||
const tasks = await this._getTasksFromList();
|
||||
tasks.forEach((task) => {
|
||||
this.dismissReminderOnly(task);
|
||||
});
|
||||
|
|
@ -307,9 +300,7 @@ export class DialogViewTaskRemindersComponent implements OnDestroy {
|
|||
|
||||
markTaskAsDone(task: TaskWithReminderData): void {
|
||||
this._taskService.setDone(task.id);
|
||||
if (task.reminderId) {
|
||||
this._removeReminderFromList(task.reminderId as string);
|
||||
}
|
||||
this._removeTaskFromList(task.id);
|
||||
}
|
||||
|
||||
async markAllAsDone(): Promise<void> {
|
||||
|
|
@ -318,7 +309,7 @@ export class DialogViewTaskRemindersComponent implements OnDestroy {
|
|||
|
||||
async markAllTasksAsDone(): Promise<void> {
|
||||
this._prepareForBulkAction();
|
||||
const tasks = await this._getTasksFromReminderList();
|
||||
const tasks = await this._getTasksFromList();
|
||||
tasks.forEach((task) => {
|
||||
this._taskService.setDone(task.id);
|
||||
});
|
||||
|
|
@ -332,18 +323,16 @@ export class DialogViewTaskRemindersComponent implements OnDestroy {
|
|||
this._matDialogRef.close();
|
||||
}
|
||||
|
||||
private _removeReminderFromList(reminderId: string): void {
|
||||
const newReminders = this.reminders$
|
||||
.getValue()
|
||||
.filter((reminder) => reminder.id !== reminderId);
|
||||
if (newReminders.length <= 0) {
|
||||
private _removeTaskFromList(taskId: string): void {
|
||||
const newTaskIds = this.taskIds$.getValue().filter((id) => id !== taskId);
|
||||
if (newTaskIds.length <= 0) {
|
||||
this._close();
|
||||
} else {
|
||||
this.reminders$.next(newReminders);
|
||||
this.taskIds$.next(newTaskIds);
|
||||
}
|
||||
}
|
||||
|
||||
private async _getTasksFromReminderList(): Promise<TaskWithReminderData[]> {
|
||||
private async _getTasksFromList(): Promise<TaskWithReminderData[]> {
|
||||
return (await this.tasks$.pipe(first()).toPromise()) as TaskWithReminderData[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import { addReminderIdToTask, removeReminderFromTask } from './task.actions';
|
||||
import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions';
|
||||
import { concatMap, filter, first, map, mergeMap, tap } from 'rxjs/operators';
|
||||
import { ReminderService } from '../../reminder/reminder.service';
|
||||
import { concatMap, filter, map, mergeMap, tap } from 'rxjs/operators';
|
||||
import { truncate } from '../../../util/truncate';
|
||||
import { T } from '../../../t.const';
|
||||
import { SnackService } from '../../../core/snack/snack.service';
|
||||
import { EMPTY } from 'rxjs';
|
||||
import { TaskService } from '../task.service';
|
||||
import { moveProjectTaskToBacklogListAuto } from '../../project/store/project.actions';
|
||||
import { flattenTasks } from './task.selectors';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { PlannerActions } from '../../planner/store/planner.actions';
|
||||
import { LocaleDatePipe } from 'src/app/ui/pipes/locale-date.pipe';
|
||||
|
||||
@Injectable()
|
||||
export class TaskReminderEffects {
|
||||
private _actions$ = inject(Actions);
|
||||
private _reminderService = inject(ReminderService);
|
||||
private _snackService = inject(SnackService);
|
||||
private _taskService = inject(TaskService);
|
||||
private _store = inject(Store);
|
||||
|
|
@ -44,27 +39,6 @@ export class TaskReminderEffects {
|
|||
{ dispatch: false },
|
||||
);
|
||||
|
||||
createReminderAndAddToTask$ = createEffect(() =>
|
||||
this._actions$.pipe(
|
||||
ofType(TaskSharedActions.scheduleTaskWithTime),
|
||||
filter(({ task, remindAt }) => typeof remindAt === 'number'),
|
||||
map(({ task, remindAt }) => {
|
||||
const reminderId = this._reminderService.addReminder(
|
||||
'TASK',
|
||||
task.id,
|
||||
truncate(task.title),
|
||||
remindAt as number,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
return addReminderIdToTask({
|
||||
taskId: task.id,
|
||||
reminderId,
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
autoMoveToBacklog$ = createEffect(() =>
|
||||
this._actions$.pipe(
|
||||
ofType(TaskSharedActions.scheduleTaskWithTime),
|
||||
|
|
@ -81,91 +55,43 @@ export class TaskReminderEffects {
|
|||
),
|
||||
);
|
||||
|
||||
updateTaskReminder$ = createEffect(() =>
|
||||
updateTaskReminderSnack$ = createEffect(
|
||||
() =>
|
||||
this._actions$.pipe(
|
||||
ofType(TaskSharedActions.reScheduleTaskWithTime),
|
||||
filter(({ remindAt }) => typeof remindAt === 'number'),
|
||||
tap(({ task }) =>
|
||||
this._snackService.open({
|
||||
type: 'SUCCESS',
|
||||
translateParams: {
|
||||
title: truncate(task.title),
|
||||
},
|
||||
msg: T.F.TASK.S.REMINDER_UPDATED,
|
||||
ico: 'schedule',
|
||||
}),
|
||||
),
|
||||
),
|
||||
{ dispatch: false },
|
||||
);
|
||||
|
||||
autoMoveToBacklogOnReschedule$ = createEffect(() =>
|
||||
this._actions$.pipe(
|
||||
ofType(TaskSharedActions.reScheduleTaskWithTime),
|
||||
filter(({ task, remindAt }) => typeof remindAt === 'number' && !!task.reminderId),
|
||||
tap(({ task, remindAt }) => {
|
||||
this._reminderService.updateReminder(task.reminderId as string, {
|
||||
remindAt,
|
||||
title: task.title,
|
||||
});
|
||||
}),
|
||||
tap(({ task }) =>
|
||||
this._snackService.open({
|
||||
type: 'SUCCESS',
|
||||
translateParams: {
|
||||
title: truncate(task.title),
|
||||
},
|
||||
msg: T.F.TASK.S.REMINDER_UPDATED,
|
||||
ico: 'schedule',
|
||||
}),
|
||||
),
|
||||
mergeMap(({ task, remindAt, dueWithTime, isMoveToBacklog }) => {
|
||||
if (isMoveToBacklog && !task.projectId) {
|
||||
throw new Error('Move to backlog not possible for non project tasks');
|
||||
}
|
||||
if (typeof remindAt !== 'number') {
|
||||
filter(({ isMoveToBacklog }) => isMoveToBacklog),
|
||||
mergeMap(({ task, isMoveToBacklog }) => {
|
||||
if (!task.projectId) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return [
|
||||
...(isMoveToBacklog
|
||||
? [
|
||||
moveProjectTaskToBacklogListAuto({
|
||||
taskId: task.id,
|
||||
projectId: task.projectId as string,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
moveProjectTaskToBacklogListAuto({
|
||||
taskId: task.id,
|
||||
projectId: task.projectId,
|
||||
}),
|
||||
];
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
clearRemindersOnDelete$ = createEffect(
|
||||
() =>
|
||||
this._actions$.pipe(
|
||||
ofType(TaskSharedActions.deleteTask),
|
||||
tap(({ task }) => {
|
||||
const deletedTaskIds = [task.id, ...task.subTaskIds];
|
||||
deletedTaskIds.forEach((id) => {
|
||||
this._reminderService.removeReminderByRelatedIdIfSet(id);
|
||||
});
|
||||
}),
|
||||
),
|
||||
{ dispatch: false },
|
||||
);
|
||||
|
||||
clearRemindersForArchivedTasks$ = createEffect(
|
||||
() =>
|
||||
this._actions$.pipe(
|
||||
ofType(TaskSharedActions.moveToArchive),
|
||||
tap(({ tasks }) => {
|
||||
const flatTasks = flattenTasks(tasks);
|
||||
if (!flatTasks.length) {
|
||||
return;
|
||||
}
|
||||
flatTasks.forEach((t) => {
|
||||
if (t.reminderId) {
|
||||
this._reminderService.removeReminder(t.reminderId);
|
||||
}
|
||||
});
|
||||
}),
|
||||
),
|
||||
{ dispatch: false },
|
||||
);
|
||||
clearMultipleReminders = createEffect(
|
||||
() =>
|
||||
this._actions$.pipe(
|
||||
ofType(TaskSharedActions.deleteTasks),
|
||||
tap(({ taskIds }) => {
|
||||
this._reminderService.removeRemindersByRelatedIds(taskIds);
|
||||
}),
|
||||
),
|
||||
{ dispatch: false },
|
||||
);
|
||||
|
||||
unscheduleDoneTask$ = createEffect(
|
||||
() =>
|
||||
this._actions$.pipe(
|
||||
|
|
@ -173,12 +99,10 @@ export class TaskReminderEffects {
|
|||
filter(({ task }) => !!task.changes.isDone),
|
||||
concatMap(({ task }) => this._taskService.getByIdOnce$(task.id as string)),
|
||||
tap((task) => {
|
||||
if (task.reminderId) {
|
||||
// TODO refactor to map with dispatch
|
||||
if (task.remindAt) {
|
||||
this._store.dispatch(
|
||||
TaskSharedActions.unscheduleTask({
|
||||
id: task.id,
|
||||
reminderId: task.reminderId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
@ -187,99 +111,34 @@ export class TaskReminderEffects {
|
|||
{ dispatch: false },
|
||||
);
|
||||
|
||||
unschedulePlannedForDayTasks$ = createEffect(() =>
|
||||
this._actions$.pipe(
|
||||
ofType(PlannerActions.transferTask),
|
||||
filter(({ task }) => !!task.reminderId),
|
||||
// delay(100),
|
||||
map(({ task }) => {
|
||||
return removeReminderFromTask({
|
||||
id: task.id,
|
||||
reminderId: task.reminderId!,
|
||||
isSkipToast: true,
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// ---------------------------------------
|
||||
// ---------------------------------------
|
||||
removeTaskReminderSideEffects$ = createEffect(
|
||||
unscheduleSnack$ = createEffect(
|
||||
() =>
|
||||
this._actions$.pipe(
|
||||
ofType(removeReminderFromTask),
|
||||
filter(({ reminderId }) => !!reminderId),
|
||||
tap(({ isSkipToast }) => {
|
||||
if (!isSkipToast) {
|
||||
this._snackService.open({
|
||||
type: 'SUCCESS',
|
||||
msg: T.F.TASK.S.REMINDER_DELETED,
|
||||
ico: 'schedule',
|
||||
});
|
||||
}
|
||||
}),
|
||||
tap(({ id, reminderId }) => {
|
||||
this._reminderService.removeReminder(reminderId as string);
|
||||
ofType(TaskSharedActions.unscheduleTask),
|
||||
filter(({ isSkipToast }) => !isSkipToast),
|
||||
tap(() => {
|
||||
this._snackService.open({
|
||||
type: 'SUCCESS',
|
||||
msg: T.F.TASK.S.REMINDER_DELETED,
|
||||
ico: 'schedule',
|
||||
});
|
||||
}),
|
||||
),
|
||||
{ dispatch: false },
|
||||
);
|
||||
removeTaskReminderTrigger1$ = createEffect(() =>
|
||||
this._actions$.pipe(
|
||||
ofType(TaskSharedActions.planTasksForToday),
|
||||
filter(({ isSkipRemoveReminder }) => !isSkipRemoveReminder),
|
||||
concatMap(({ taskIds }) => this._taskService.getByIdsLive$(taskIds).pipe(first())),
|
||||
mergeMap((tasks) =>
|
||||
tasks
|
||||
.filter((task) => !!task.reminderId)
|
||||
.map((task) =>
|
||||
removeReminderFromTask({
|
||||
id: task.id,
|
||||
reminderId: task.reminderId as string,
|
||||
isSkipToast: true,
|
||||
}),
|
||||
),
|
||||
|
||||
dismissReminderSnack$ = createEffect(
|
||||
() =>
|
||||
this._actions$.pipe(
|
||||
ofType(TaskSharedActions.dismissReminderOnly),
|
||||
tap(() => {
|
||||
this._snackService.open({
|
||||
type: 'SUCCESS',
|
||||
msg: T.F.TASK.S.REMINDER_DELETED,
|
||||
ico: 'schedule',
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
{ dispatch: false },
|
||||
);
|
||||
removeTaskReminderTrigger2$ = createEffect(() =>
|
||||
this._actions$.pipe(
|
||||
ofType(TaskSharedActions.unscheduleTask),
|
||||
filter(({ reminderId }) => !!reminderId),
|
||||
map(({ id, reminderId }) => {
|
||||
return removeReminderFromTask({
|
||||
id,
|
||||
reminderId: reminderId as string,
|
||||
isSkipToast: true,
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
removeTaskReminderForDismissOnly$ = createEffect(() =>
|
||||
this._actions$.pipe(
|
||||
ofType(TaskSharedActions.dismissReminderOnly),
|
||||
map(({ id, reminderId }) => {
|
||||
return removeReminderFromTask({
|
||||
id,
|
||||
reminderId: reminderId,
|
||||
isSkipToast: false,
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
removeTaskReminderTrigger3$ = createEffect(() => {
|
||||
return this._actions$.pipe(
|
||||
ofType(PlannerActions.planTaskForDay),
|
||||
filter(({ task, day }) => !!task.reminderId),
|
||||
map(({ task }) => {
|
||||
return removeReminderFromTask({
|
||||
id: task.id,
|
||||
reminderId: task.reminderId as string,
|
||||
isSkipToast: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,14 +23,6 @@ export const setSelectedTask = createAction(
|
|||
|
||||
export const unsetCurrentTask = createAction('[Task] UnsetCurrentTask');
|
||||
|
||||
export const addReminderIdToTask = createAction(
|
||||
'[Task] Add ReminderId to Task',
|
||||
props<{
|
||||
taskId: string;
|
||||
reminderId: string;
|
||||
}>(),
|
||||
);
|
||||
|
||||
export const __updateMultipleTaskSimple = createAction(
|
||||
'[Task] Update multiple Tasks (simple)',
|
||||
props<{
|
||||
|
|
@ -139,17 +131,6 @@ export const removeTimeSpent = createAction(
|
|||
}),
|
||||
);
|
||||
|
||||
export const removeReminderFromTask = createAction(
|
||||
'[Task] Remove Reminder',
|
||||
|
||||
props<{
|
||||
id: string;
|
||||
reminderId: string;
|
||||
isSkipToast?: boolean;
|
||||
isLeaveDueTime?: boolean;
|
||||
}>(),
|
||||
);
|
||||
|
||||
export const addSubTask = createAction(
|
||||
'[Task] Add SubTask',
|
||||
(taskProps: { task: Task; parentId: string }) => ({
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import {
|
||||
__updateMultipleTaskSimple,
|
||||
addReminderIdToTask,
|
||||
addSubTask,
|
||||
moveSubTask,
|
||||
moveSubTaskDown,
|
||||
moveSubTaskToBottom,
|
||||
moveSubTaskToTop,
|
||||
moveSubTaskUp,
|
||||
removeReminderFromTask,
|
||||
removeTimeSpent,
|
||||
roundTimeSpentForDay,
|
||||
setCurrentTask,
|
||||
|
|
@ -646,35 +644,4 @@ export const taskReducer = createReducer<TaskState>(
|
|||
ids: [...taskIds, ...state.ids.filter((id) => !taskIds.includes(id))],
|
||||
};
|
||||
}),
|
||||
|
||||
// REMINDER STUFF
|
||||
// --------------
|
||||
on(addReminderIdToTask, (state, { taskId, reminderId }) => {
|
||||
return taskAdapter.updateOne(
|
||||
{
|
||||
id: taskId,
|
||||
changes: {
|
||||
reminderId,
|
||||
},
|
||||
},
|
||||
state,
|
||||
);
|
||||
}),
|
||||
|
||||
on(removeReminderFromTask, (state, { id, isLeaveDueTime }) => {
|
||||
return taskAdapter.updateOne(
|
||||
{
|
||||
id,
|
||||
changes: {
|
||||
reminderId: undefined,
|
||||
...(isLeaveDueTime
|
||||
? {}
|
||||
: {
|
||||
dueWithTime: undefined,
|
||||
}),
|
||||
},
|
||||
},
|
||||
state,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
TaskState,
|
||||
TaskWithDueDay,
|
||||
TaskWithDueTime,
|
||||
TaskWithReminder,
|
||||
TaskWithSubTasks,
|
||||
} from '../task.model';
|
||||
import { taskAdapter } from './task.adapter';
|
||||
|
|
@ -481,6 +482,15 @@ export const selectAllTasksWithDueTimeSorted = createSelector(
|
|||
},
|
||||
);
|
||||
|
||||
export const selectAllTasksWithReminder = createSelector(
|
||||
selectAllTasks,
|
||||
(tasks: Task[]): TaskWithReminder[] => {
|
||||
return tasks.filter(
|
||||
(task) => typeof task.remindAt === 'number' && !task.isDone,
|
||||
) as TaskWithReminder[];
|
||||
},
|
||||
);
|
||||
|
||||
export const selectTasksWithDueTimeUntil = createSelector(
|
||||
selectAllTasks,
|
||||
(tasks: Task[], props: { end: number }): TaskWithDueTime[] => {
|
||||
|
|
|
|||
|
|
@ -402,7 +402,6 @@ export class TaskContextMenuInnerComponent implements AfterViewInit {
|
|||
this._store.dispatch(
|
||||
TaskSharedActions.unscheduleTask({
|
||||
id: this.task.id,
|
||||
reminderId: this.task.reminderId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
@ -656,7 +655,6 @@ export class TaskContextMenuInnerComponent implements AfterViewInit {
|
|||
this._store.dispatch(
|
||||
TaskSharedActions.unscheduleTask({
|
||||
id: this.task.id,
|
||||
reminderId: this.task.reminderId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { IssueProviderKey } from '../issue/issue.model';
|
||||
import { Reminder } from '../reminder/reminder.model';
|
||||
import { EntityState } from '@ngrx/entity';
|
||||
import { TaskAttachment } from './task-attachment/task-attachment.model';
|
||||
// Import the unified Task type from plugin-api
|
||||
|
|
@ -97,7 +96,9 @@ export interface TaskCopy
|
|||
modified?: number;
|
||||
doneOn?: number;
|
||||
parentId?: string;
|
||||
/** @deprecated Use remindAt instead. Kept for migration compatibility. */
|
||||
reminderId?: string;
|
||||
remindAt?: number;
|
||||
repeatCfgId?: string;
|
||||
_hideSubTasksMode?: HideSubTasksMode;
|
||||
}
|
||||
|
|
@ -113,13 +114,12 @@ export type ArchiveTask = Readonly<TaskCopy>;
|
|||
export type Task = Readonly<TaskCopy>;
|
||||
|
||||
export interface TaskWithReminderData extends Task {
|
||||
readonly reminderData: Reminder;
|
||||
readonly reminderData: { remindAt: number };
|
||||
readonly parentData?: Task;
|
||||
}
|
||||
|
||||
export interface TaskWithReminder extends Task {
|
||||
reminderId: string;
|
||||
dueWithTime: number;
|
||||
remindAt: number;
|
||||
}
|
||||
|
||||
export interface TaskWithDueTime extends Task {
|
||||
|
|
@ -133,8 +133,7 @@ export interface TaskWithDueDay extends Task {
|
|||
export type TaskPlannedWithDayOrTime = TaskWithDueTime | TaskWithDueDay;
|
||||
|
||||
export interface TaskWithoutReminder extends Task {
|
||||
reminderId: undefined;
|
||||
due: undefined;
|
||||
remindAt: undefined;
|
||||
}
|
||||
|
||||
export interface TaskWithPlannedForDayIndication extends TaskWithoutReminder {
|
||||
|
|
|
|||
|
|
@ -650,7 +650,6 @@ export class TaskComponent implements OnDestroy, AfterViewInit {
|
|||
this._store.dispatch(
|
||||
TaskSharedActions.unscheduleTask({
|
||||
id: this.task().id,
|
||||
reminderId: this.task().reminderId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ export const flushYoungToOld = createAction(
|
|||
isPersistent: true,
|
||||
entityType: 'ALL', // Affects multiple entity types (tasks, timeTracking)
|
||||
opType: OpType.Batch, // Bulk operation affecting multiple entities
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,6 @@ export const syncTimeSpent = createAction(
|
|||
entityType: 'TASK',
|
||||
entityId: actionProps.taskId,
|
||||
opType: OpType.Update,
|
||||
} as PersistentActionMeta,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
80
src/app/pfapi/migrate/cross-model-4_6.ts
Normal file
80
src/app/pfapi/migrate/cross-model-4_6.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { AppDataCompleteNew } from '../pfapi-config';
|
||||
import { CrossModelMigrateFn } from '../api';
|
||||
import { PFLog } from '../../core/log';
|
||||
|
||||
interface LegacyReminder {
|
||||
id: string;
|
||||
remindAt: number;
|
||||
title: string;
|
||||
type: 'NOTE' | 'TASK';
|
||||
relatedId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration 4.6: Move reminders to task.remindAt
|
||||
*
|
||||
* This migration:
|
||||
* 1. Reads all reminders from the legacy reminders array
|
||||
* 2. For TASK reminders, updates the corresponding task's remindAt field
|
||||
* 3. Discards NOTE reminders (feature discontinued)
|
||||
* 4. Clears the reminders array
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const crossModelMigration4_6: CrossModelMigrateFn = ((
|
||||
fullData: AppDataCompleteNew,
|
||||
): AppDataCompleteNew => {
|
||||
PFLog.log('____________________Migrate4.6 (Reminder Migration)__________________');
|
||||
const copy = fullData;
|
||||
|
||||
const reminders = (copy.reminders || []) as LegacyReminder[];
|
||||
|
||||
if (reminders.length === 0) {
|
||||
PFLog.log('No reminders to migrate');
|
||||
return copy;
|
||||
}
|
||||
|
||||
PFLog.log(`Migrating ${reminders.length} reminders to task.remindAt`);
|
||||
|
||||
let migratedCount = 0;
|
||||
let skippedNotes = 0;
|
||||
let skippedMissing = 0;
|
||||
|
||||
for (const reminder of reminders) {
|
||||
if (reminder.type === 'NOTE') {
|
||||
// Note reminders are discontinued
|
||||
skippedNotes++;
|
||||
PFLog.log(`Skipping NOTE reminder: ${reminder.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reminder.type === 'TASK') {
|
||||
const task = copy.task?.entities?.[reminder.relatedId];
|
||||
if (task) {
|
||||
// Update the task with remindAt
|
||||
// @ts-ignore - modifying readonly entity
|
||||
copy.task.entities[reminder.relatedId] = {
|
||||
...task,
|
||||
remindAt: reminder.remindAt,
|
||||
// Clear the old reminderId
|
||||
reminderId: undefined,
|
||||
};
|
||||
migratedCount++;
|
||||
PFLog.log(`Migrated reminder for task: ${reminder.relatedId}`);
|
||||
} else {
|
||||
// Task not found (might be archived or deleted)
|
||||
skippedMissing++;
|
||||
PFLog.log(`Task not found for reminder: ${reminder.relatedId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the reminders array
|
||||
// @ts-ignore
|
||||
copy.reminders = [];
|
||||
|
||||
PFLog.log(
|
||||
`Migration complete: ${migratedCount} migrated, ${skippedNotes} NOTE reminders skipped, ${skippedMissing} missing tasks`,
|
||||
);
|
||||
PFLog.log(copy);
|
||||
return copy;
|
||||
}) as CrossModelMigrateFn;
|
||||
|
|
@ -27,6 +27,7 @@ const handleScheduleTaskWithTime = (
|
|||
state: RootState,
|
||||
task: { id: string },
|
||||
dueWithTime: number,
|
||||
remindAt?: number,
|
||||
): RootState => {
|
||||
// Check if task already has the same dueWithTime
|
||||
const currentTask = state[TASK_FEATURE_NAME].entities[task.id] as Task;
|
||||
|
|
@ -41,6 +42,7 @@ const handleScheduleTaskWithTime = (
|
|||
// If task is already correctly scheduled, don't change state
|
||||
if (
|
||||
currentTask.dueWithTime === dueWithTime &&
|
||||
currentTask.remindAt === remindAt &&
|
||||
isScheduledForToday === isCurrentlyInToday
|
||||
) {
|
||||
return state;
|
||||
|
|
@ -55,6 +57,7 @@ const handleScheduleTaskWithTime = (
|
|||
changes: {
|
||||
dueWithTime,
|
||||
dueDay: undefined,
|
||||
remindAt,
|
||||
},
|
||||
},
|
||||
state[TASK_FEATURE_NAME],
|
||||
|
|
@ -92,6 +95,7 @@ const handleUnScheduleTask = (
|
|||
changes: {
|
||||
dueDay: isLeaveInToday ? getDbDateStr() : undefined,
|
||||
dueWithTime: undefined,
|
||||
remindAt: undefined,
|
||||
},
|
||||
},
|
||||
state[TASK_FEATURE_NAME],
|
||||
|
|
@ -115,14 +119,14 @@ const handleUnScheduleTask = (
|
|||
};
|
||||
|
||||
const handleDismissReminderOnly = (state: RootState, taskId: string): RootState => {
|
||||
// Only clear the dueWithTime (reminder time) but keep dueDay and Today tag
|
||||
// Only clear remindAt (the reminder notification) but keep dueWithTime, dueDay, and Today tag
|
||||
return {
|
||||
...state,
|
||||
[TASK_FEATURE_NAME]: taskAdapter.updateOne(
|
||||
{
|
||||
id: taskId,
|
||||
changes: {
|
||||
dueWithTime: undefined,
|
||||
remindAt: undefined,
|
||||
},
|
||||
},
|
||||
state[TASK_FEATURE_NAME],
|
||||
|
|
@ -157,7 +161,7 @@ const handlePlanTasksForToday = (
|
|||
id: taskId,
|
||||
changes: {
|
||||
dueDay: today,
|
||||
...(shouldClearTime ? { dueWithTime: undefined } : {}),
|
||||
...(shouldClearTime ? { dueWithTime: undefined, remindAt: undefined } : {}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -243,16 +247,16 @@ const handleMoveTaskInTodayTagList = (
|
|||
|
||||
const createActionHandlers = (state: RootState, action: Action): ActionHandlerMap => ({
|
||||
[TaskSharedActions.scheduleTaskWithTime.type]: () => {
|
||||
const { task, dueWithTime } = action as ReturnType<
|
||||
const { task, dueWithTime, remindAt } = action as ReturnType<
|
||||
typeof TaskSharedActions.scheduleTaskWithTime
|
||||
>;
|
||||
return handleScheduleTaskWithTime(state, task, dueWithTime);
|
||||
return handleScheduleTaskWithTime(state, task, dueWithTime, remindAt);
|
||||
},
|
||||
[TaskSharedActions.reScheduleTaskWithTime.type]: () => {
|
||||
const { task, dueWithTime } = action as ReturnType<
|
||||
const { task, dueWithTime, remindAt } = action as ReturnType<
|
||||
typeof TaskSharedActions.reScheduleTaskWithTime
|
||||
>;
|
||||
return handleScheduleTaskWithTime(state, task, dueWithTime);
|
||||
return handleScheduleTaskWithTime(state, task, dueWithTime, remindAt);
|
||||
},
|
||||
[TaskSharedActions.unscheduleTask.type]: () => {
|
||||
const { id, isLeaveInToday } = action as ReturnType<
|
||||
|
|
|
|||
|
|
@ -126,7 +126,6 @@ export const TaskSharedActions = createActionGroup({
|
|||
|
||||
unscheduleTask: (taskProps: {
|
||||
id: string;
|
||||
reminderId?: string;
|
||||
isSkipToast?: boolean;
|
||||
isLeaveInToday?: boolean;
|
||||
}) => ({
|
||||
|
|
@ -139,7 +138,7 @@ export const TaskSharedActions = createActionGroup({
|
|||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
|
||||
dismissReminderOnly: (taskProps: { id: string; reminderId: string }) => ({
|
||||
dismissReminderOnly: (taskProps: { id: string }) => ({
|
||||
...taskProps,
|
||||
meta: {
|
||||
isPersistent: true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue