= {
label: T.F.SYNC.FORM.L_SYNC_PROVIDER,
required: true,
options: [
- { label: SyncProvider.Dropbox, value: SyncProvider.Dropbox },
- { label: SyncProvider.WebDAV, value: SyncProvider.WebDAV },
- ...(IS_ELECTRON ||
- (IS_ANDROID_WEB_VIEW &&
- (androidInterface as any).grantFilePermission &&
- (androidInterface as any).isGrantedFilePermission)
- ? [{ label: SyncProvider.LocalFile, value: SyncProvider.LocalFile }]
+ { label: LegacySyncProvider.Dropbox, value: LegacySyncProvider.Dropbox },
+ { label: LegacySyncProvider.WebDAV, value: LegacySyncProvider.WebDAV },
+ ...(IS_ELECTRON || IS_ANDROID_WEB_VIEW
+ ? [
+ {
+ label: LegacySyncProvider.LocalFile,
+ value: LegacySyncProvider.LocalFile,
+ },
+ ]
: []),
],
- change: (field, ev) => {
- if (
- IS_ANDROID_WEB_VIEW &&
- field.model.syncProvider === SyncProvider.LocalFile
- ) {
- // disable / enable is a workaround for the hide expression for the info file path info tpl
- field.formControl?.disable();
-
- androidInterface.grantFilePermissionWrapped().then(() => {
- field.formControl?.enable();
- console.log('Granted file access permission for android');
- console.log(androidInterface?.allowedFolderPath());
- field.formControl?.updateValueAndValidity();
- field.formControl?.parent?.updateValueAndValidity();
- field.formControl?.parent?.markAllAsTouched();
- field.formControl?.markAllAsTouched();
- });
- }
- },
- },
- validators: {
- validFileAccessPermission: {
- expression: (c: any) => {
- if (IS_ANDROID_WEB_VIEW && c.value === SyncProvider.LocalFile) {
- console.log(
- 'Checking file access permission for android',
- androidInterface.isGrantedFilePermission(),
- );
- return androidInterface.isGrantedFilePermission();
- }
- return true;
- },
- message: T.F.SYNC.FORM.LOCAL_FILE.L_SYNC_FILE_PATH_PERMISSION_VALIDATION,
- },
- },
- validation: {
- show: true,
},
},
- // TODO remove completely
- // {
- // // TODO animation maybe
- // hideExpression: (m, v, field) =>
- // field?.parent?.model.syncProvider !== SyncProvider.Dropbox,
- // key: 'dropboxSync',
- // fieldGroup: [
- // {
- // key: 'accessToken',
- // type: 'input',
- // hideExpression: (model: DropboxSyncConfig) => !model?.accessToken,
- // templateOptions: {
- // label: T.F.SYNC.FORM.DROPBOX.L_ACCESS_TOKEN,
- // },
- // },
- // ],
- // },
- IS_ANDROID_WEB_VIEW
- ? {
- hideExpression: (m, v, field) => {
- return (
- !IS_ANDROID_WEB_VIEW ||
- field?.parent?.model.syncProvider !== SyncProvider.LocalFile ||
- !androidInterface?.isGrantedFilePermission() ||
- !androidInterface?.allowedFolderPath()
- );
- },
- type: 'tpl',
- className: `tpl`,
- expressionProperties: {
- template: () =>
- // NOTE: hard to translate here, that's why we don't
- `Granted file access permission:
${
- androidInterface.allowedFolderPath && androidInterface.allowedFolderPath()
- }
`,
- },
- }
- : {},
{
hideExpression: (m, v, field) =>
- field?.parent?.model.syncProvider !== SyncProvider.LocalFile ||
+ field?.parent?.model.syncProvider !== LegacySyncProvider.LocalFile ||
// hide for android
IS_ANDROID_WEB_VIEW,
key: 'localFileSync',
fieldGroup: [
{
+ type: 'btn',
key: 'syncFolderPath',
- type: 'input',
templateOptions: {
+ text: T.F.SYNC.FORM.LOCAL_FILE.L_SYNC_FOLDER_PATH,
required: true,
- label: T.F.SYNC.FORM.LOCAL_FILE.L_SYNC_FOLDER_PATH,
+ onClick: () => {
+ return fileSyncElectron.pickDirectory();
+ },
},
},
],
},
{
hideExpression: (m, v, field) =>
- field?.parent?.model.syncProvider !== SyncProvider.WebDAV,
+ field?.parent?.model.syncProvider !== LegacySyncProvider.WebDAV,
key: 'webDav',
fieldGroup: [
...(!IS_ELECTRON && !IS_ANDROID_WEB_VIEW
@@ -140,7 +70,6 @@ export const SYNC_FORM: ConfigFormSection = {
type: 'tpl',
templateOptions: {
tag: 'p',
- // text: `Please open the following link and copy the auth code provided there
`,
text: T.F.SYNC.FORM.WEB_DAV.CORS_INFO,
},
},
@@ -227,7 +156,7 @@ export const SYNC_FORM: ConfigFormSection = {
},
{
hideExpression: (model: any) => !model.isEncryptionEnabled,
- key: 'encryptionPassword',
+ key: 'encryptKey',
type: 'input',
templateOptions: {
required: true,
diff --git a/src/app/features/config/form-cfgs/take-a-break-form.const.ts b/src/app/features/config/form-cfgs/take-a-break-form.const.ts
index 1dac4c71683..f3c54e2d91c 100644
--- a/src/app/features/config/form-cfgs/take-a-break-form.const.ts
+++ b/src/app/features/config/form-cfgs/take-a-break-form.const.ts
@@ -103,6 +103,7 @@ export const TAKE_A_BREAK_FORM_CFG: ConfigFormSection = {
addText: T.GCF.TAKE_A_BREAK.ADD_NEW_IMG,
required: true,
defaultValue: '',
+ minLength: 3,
},
fieldArray: {
type: 'input',
diff --git a/src/app/features/config/global-config-form-config.const.ts b/src/app/features/config/global-config-form-config.const.ts
index 1d7de47d030..9ebd22289a5 100644
--- a/src/app/features/config/global-config-form-config.const.ts
+++ b/src/app/features/config/global-config-form-config.const.ts
@@ -9,7 +9,6 @@ import { LANGUAGE_SELECTION_FORM_FORM } from './form-cfgs/language-selection-for
import { EVALUATION_SETTINGS_FORM_CFG } from './form-cfgs/evaluation-settings-form.const';
import { SIMPLE_COUNTER_FORM } from './form-cfgs/simple-counter-form.const';
import { TIME_TRACKING_FORM_CFG } from './form-cfgs/time-tracking-form.const';
-import { SYNC_FORM } from './form-cfgs/sync-form.const';
import { IS_ELECTRON } from '../../app.constants';
import { IS_ANDROID_WEB_VIEW } from '../../util/is-android-web-view';
import { SCHEDULE_FORM_CFG } from './form-cfgs/schedule-form.const';
@@ -37,8 +36,7 @@ export const GLOBAL_CONFIG_FORM_CONFIG: ConfigFormConfig = [
SCHEDULE_FORM_CFG,
].filter(filterGlobalConfigForm);
-export const GLOBAL_SYNC_FORM_CONFIG: ConfigFormConfig = [
- SYNC_FORM,
+export const GLOBAL_IMEX_FORM_CONFIG: ConfigFormConfig = [
// NOTE: the backup form is added dynamically due to async prop required
...(IS_ANDROID_WEB_VIEW ? [] : [IMEX_FORM]),
].filter(filterGlobalConfigForm);
diff --git a/src/app/features/config/global-config.model.ts b/src/app/features/config/global-config.model.ts
index d99ca8b73d9..c5ed70409a6 100644
--- a/src/app/features/config/global-config.model.ts
+++ b/src/app/features/config/global-config.model.ts
@@ -1,7 +1,7 @@
import { FormlyFieldConfig } from '@ngx-formly/core';
import { ProjectCfgFormKey } from '../project/project.model';
import { LanguageCode, MODEL_VERSION_KEY } from '../../app.constants';
-import { SyncProvider } from '../../imex/sync/sync-provider.model';
+import { LegacySyncProvider } from '../../imex/sync/legacy-sync-provider.model';
import { KeyboardConfig } from './keyboard-config.model';
import { LegacyCalendarProvider } from '../issue/providers/calendar/calendar.model';
@@ -34,14 +34,14 @@ export type ShortSyntaxConfig = Readonly<{
export type TimeTrackingConfig = Readonly<{
trackingInterval: number;
defaultEstimate: number;
- defaultEstimateSubTasks: number;
+ defaultEstimateSubTasks?: number | null;
isAutoStartNextTask: boolean;
isNotifyWhenTimeEstimateExceeded: boolean;
isTrackingReminderEnabled: boolean;
isTrackingReminderShowOnMobile: boolean;
trackingReminderMinTime: number;
- isTrackingReminderNotify: boolean;
- isTrackingReminderFocusWindow: boolean;
+ isTrackingReminderNotify?: boolean;
+ isTrackingReminderFocusWindow?: boolean;
}>;
export type EvaluationConfig = Readonly<{
@@ -63,7 +63,8 @@ export type TakeABreakConfig = Readonly<{
takeABreakMessage: string;
takeABreakMinWorkingTime: number;
takeABreakSnoozeTime: number;
- motivationalImgs: string[];
+ // due to ngx-formly inconsistency they also can be undefined or null even
+ motivationalImgs: (string | undefined | null)[];
}>;
export type PomodoroConfig = Readonly<{
@@ -84,8 +85,7 @@ export type PomodoroConfig = Readonly<{
}>;
// NOTE: needs to be writable due to how we use it
-
-export type DropboxSyncConfig = object;
+// export type DropboxSyncConfig = object;
export interface WebDavConfig {
baseUrl: string | null;
@@ -114,21 +114,23 @@ export type SoundConfig = Readonly<{
isIncreaseDoneSoundPitch: boolean;
doneSound: string | null;
breakReminderSound: string | null;
- trackTimeSound: string | null;
+ trackTimeSound?: string | null;
volume: number;
}>;
export type SyncConfig = Readonly<{
isEnabled: boolean;
isEncryptionEnabled: boolean;
- encryptionPassword: string | null;
isCompressionEnabled: boolean;
- syncProvider: SyncProvider | null;
+ syncProvider: LegacySyncProvider | null;
syncInterval: number;
- dropboxSync: DropboxSyncConfig;
- webDav: WebDavConfig;
- localFileSync: LocalFileSyncConfig;
+ /* NOTE: view model for form only*/
+ encryptKey?: string | null;
+ /* NOTE: view model for form only*/
+ webDav?: WebDavConfig;
+ /* NOTE: view model for form only*/
+ localFileSync?: LocalFileSyncConfig;
}>;
export type LegacyCalendarIntegrationConfig = Readonly<{
calendarProviders: LegacyCalendarProvider[];
diff --git a/src/app/features/config/migrate-global-config.util.ts b/src/app/features/config/migrate-global-config.util.ts
index 64d2fee08da..97fa1791b32 100644
--- a/src/app/features/config/migrate-global-config.util.ts
+++ b/src/app/features/config/migrate-global-config.util.ts
@@ -8,7 +8,7 @@ import {
import { DEFAULT_GLOBAL_CONFIG } from './default-global-config.const';
import { MODEL_VERSION_KEY } from '../../app.constants';
import { isMigrateModel } from '../../util/is-migrate-model';
-import { SyncProvider } from '../../imex/sync/sync-provider.model';
+import { LegacySyncProvider } from '../../imex/sync/legacy-sync-provider.model';
import { MODEL_VERSION } from '../../core/model-version';
export const migrateGlobalConfigState = (
@@ -208,7 +208,7 @@ const _migrateSyncCfg = (config: GlobalConfigState): GlobalConfigState => {
};
if (config.sync) {
- let syncProvider: SyncProvider | null = config.sync.syncProvider;
+ let syncProvider: LegacySyncProvider | null = config.sync.syncProvider;
if ((syncProvider as any) === 'GoogleDrive') {
syncProvider = null;
}
@@ -216,7 +216,7 @@ const _migrateSyncCfg = (config: GlobalConfigState): GlobalConfigState => {
delete (config.sync as any).googleDriveSync;
}
- if (!config.sync.localFileSync || !config.sync.dropboxSync || !config.sync.webDav) {
+ if (!config.sync.localFileSync || !config.sync.webDav) {
console.warn(
'sync config was missing some keys, reverting to default',
config.sync,
diff --git a/src/app/features/config/store/global-config.effects.ts b/src/app/features/config/store/global-config.effects.ts
index c7e5bfc3d1e..64c5bf68388 100644
--- a/src/app/features/config/store/global-config.effects.ts
+++ b/src/app/features/config/store/global-config.effects.ts
@@ -1,9 +1,8 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { filter, tap, withLatestFrom } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { CONFIG_FEATURE_NAME } from './global-config.reducer';
-import { PersistenceService } from '../../../core/persistence/persistence.service';
import { IS_ELECTRON, LanguageCode } from '../../../app.constants';
import { T } from '../../../t.const';
import { LanguageService } from '../../../core/language/language.service';
@@ -15,11 +14,12 @@ import { KeyboardConfig } from '../keyboard-config.model';
import { updateGlobalConfigSection } from './global-config.actions';
import { MiscConfig } from '../global-config.model';
import { hideSideNav, toggleSideNav } from '../../../core-ui/layout/store/layout.actions';
+import { PfapiService } from '../../../pfapi/pfapi.service';
@Injectable()
export class GlobalConfigEffects {
private _actions$ = inject(Actions);
- private _persistenceService = inject(PersistenceService);
+ private _pfapiService = inject(PfapiService);
private _languageService = inject(LanguageService);
private _dateService = inject(DateService);
private _snackService = inject(SnackService);
@@ -172,8 +172,8 @@ export class GlobalConfigEffects {
{ isSkipSyncModelChangeUpdate } = { isSkipSyncModelChangeUpdate: false },
): void {
const globalConfig = completeState[CONFIG_FEATURE_NAME];
- this._persistenceService.globalConfig.saveState(globalConfig, {
- isSyncModelChange: !isSkipSyncModelChangeUpdate,
+ this._pfapiService.m.globalConfig.save(globalConfig, {
+ isUpdateRevAndLastUpdate: !isSkipSyncModelChangeUpdate,
});
}
}
diff --git a/src/app/features/finish-day-before-close/finish-day-before-close.effects.ts b/src/app/features/finish-day-before-close/finish-day-before-close.effects.ts
index f589d3c59fb..2ec63fbe5af 100644
--- a/src/app/features/finish-day-before-close/finish-day-before-close.effects.ts
+++ b/src/app/features/finish-day-before-close/finish-day-before-close.effects.ts
@@ -10,7 +10,6 @@ import {
switchMap,
tap,
} from 'rxjs/operators';
-import { DataInitService } from '../../core/data-init/data-init.service';
import { IS_ELECTRON } from '../../app.constants';
import { EMPTY, Observable } from 'rxjs';
import { WorkContextService } from '../work-context/work-context.service';
@@ -19,6 +18,7 @@ import { Router } from '@angular/router';
import { TODAY_TAG } from '../tag/tag.const';
import { TranslateService } from '@ngx-translate/core';
import { T } from '../../t.const';
+import { DataInitStateService } from '../../core/data-init/data-init-state.service';
const EXEC_BEFORE_CLOSE_ID = 'FINISH_DAY_BEFORE_CLOSE_EFFECT';
@@ -27,17 +27,18 @@ export class FinishDayBeforeCloseEffects {
private actions$ = inject(Actions);
private _execBeforeCloseService = inject(ExecBeforeCloseService);
private _globalConfigService = inject(GlobalConfigService);
- private _dataInitService = inject(DataInitService);
+ private _dataInitStateService = inject(DataInitStateService);
private _taskService = inject(TaskService);
private _workContextService = inject(WorkContextService);
private _router = inject(Router);
private _translateService = inject(TranslateService);
- isEnabled$: Observable = this._dataInitService.isAllDataLoadedInitially$.pipe(
- concatMap(() => this._globalConfigService.misc$),
- map((misc) => misc.isConfirmBeforeExitWithoutFinishDay),
- distinctUntilChanged(),
- );
+ isEnabled$: Observable =
+ this._dataInitStateService.isAllDataLoadedInitially$.pipe(
+ concatMap(() => this._globalConfigService.misc$),
+ map((misc) => misc.isConfirmBeforeExitWithoutFinishDay),
+ distinctUntilChanged(),
+ );
scheduleUnscheduleFinishDayBeforeClose$ =
IS_ELECTRON &&
diff --git a/src/app/features/issue/providers/calendar/calendar-common-interfaces.service.ts b/src/app/features/issue/providers/calendar/calendar-common-interfaces.service.ts
index 1d127bde37a..2ce635b86fd 100644
--- a/src/app/features/issue/providers/calendar/calendar-common-interfaces.service.ts
+++ b/src/app/features/issue/providers/calendar/calendar-common-interfaces.service.ts
@@ -9,9 +9,9 @@ import {
SearchResultItem,
} from '../../issue.model';
import { CalendarIntegrationService } from '../../../calendar-integration/calendar-integration.service';
-import { catchError, map, switchMap } from 'rxjs/operators';
+import { map, switchMap } from 'rxjs/operators';
import { IssueProviderService } from '../../issue-provider.service';
-import { ICalIssueReduced, CalendarProviderCfg } from './calendar.model';
+import { CalendarProviderCfg, ICalIssueReduced } from './calendar.model';
import { HttpClient } from '@angular/common/http';
import { ICAL_TYPE } from '../../issue.const';
@@ -35,14 +35,7 @@ export class CalendarCommonInterfacesService implements IssueServiceInterface {
}
testConnection$(cfg: CalendarProviderCfg): Observable {
- // simple http get request
- return this._http.get(cfg.icalUrl, { responseType: 'text' }).pipe(
- map((v) => !!v),
- catchError((err) => {
- console.error(err);
- return of(false);
- }),
- );
+ return this._calendarIntegrationService.testConnection$(cfg);
}
getById$(id: number, issueProviderId: string): Observable {
diff --git a/src/app/features/issue/providers/gitea/gitea.const.ts b/src/app/features/issue/providers/gitea/gitea.const.ts
index f24a5c4ed65..55dac1b2640 100644
--- a/src/app/features/issue/providers/gitea/gitea.const.ts
+++ b/src/app/features/issue/providers/gitea/gitea.const.ts
@@ -1,11 +1,11 @@
-import {
- ConfigFormSection,
- LimitedFormlyFieldConfig,
-} from 'src/app/features/config/global-config.model';
-import { T } from 'src/app/t.const';
import { GiteaCfg } from './gitea.model';
import { IssueProviderGitea } from '../../issue.model';
import { ISSUE_PROVIDER_COMMON_FORM_FIELDS } from '../../common-issue-form-stuff.const';
+import {
+ ConfigFormSection,
+ LimitedFormlyFieldConfig,
+} from '../../../config/global-config.model';
+import { T } from '../../../../t.const';
export const GITEA_POLL_INTERVAL = 5 * 60 * 1000;
export const GITEA_INITIAL_POLL_DELAY = 8 * 1000;
diff --git a/src/app/features/issue/providers/github/github.model.ts b/src/app/features/issue/providers/github/github.model.ts
index 1425e71fff7..38caa8107a1 100644
--- a/src/app/features/issue/providers/github/github.model.ts
+++ b/src/app/features/issue/providers/github/github.model.ts
@@ -3,6 +3,6 @@ import { BaseIssueProviderCfg } from '../../issue.model';
export interface GithubCfg extends BaseIssueProviderCfg {
repo: string | null;
token: string | null;
- filterUsernameForIssueUpdates: string | null;
- backlogQuery: string;
+ filterUsernameForIssueUpdates?: string | null;
+ backlogQuery?: string;
}
diff --git a/src/app/features/issue/providers/jira/jira-api-responses.d.ts b/src/app/features/issue/providers/jira/jira-api-responses.d.ts
index 7441866101a..e788fd1cce6 100644
--- a/src/app/features/issue/providers/jira/jira-api-responses.d.ts
+++ b/src/app/features/issue/providers/jira/jira-api-responses.d.ts
@@ -166,7 +166,7 @@ export type JiraOriginalTransition = Readonly<{
id: string;
statusCategory: {
self: string;
- id: 2;
+ id: number;
key: string;
colorName: string;
name: string;
diff --git a/src/app/features/issue/providers/jira/jira-issue/jira-issue.effects.ts b/src/app/features/issue/providers/jira/jira-issue/jira-issue.effects.ts
index 4e54a340930..db360d743f3 100644
--- a/src/app/features/issue/providers/jira/jira-issue/jira-issue.effects.ts
+++ b/src/app/features/issue/providers/jira/jira-issue/jira-issue.effects.ts
@@ -270,7 +270,10 @@ export class JiraIssueEffects {
concatMap((issue) => this._openTransitionDialog(issue, localState, task)),
);
default:
- if (!chosenTransition || !chosenTransition.id) {
+ if (
+ !chosenTransition ||
+ !(typeof chosenTransition === 'object' && chosenTransition?.id)
+ ) {
this._snackService.open({
msg: T.F.JIRA.S.NO_VALID_TRANSITION,
type: 'ERROR',
diff --git a/src/app/features/issue/providers/jira/jira.model.ts b/src/app/features/issue/providers/jira/jira.model.ts
index f6ccd1e82de..b9648790399 100644
--- a/src/app/features/issue/providers/jira/jira.model.ts
+++ b/src/app/features/issue/providers/jira/jira.model.ts
@@ -1,7 +1,12 @@
import { JiraOriginalTransition } from './jira-api-responses';
import { BaseIssueProviderCfg } from '../../issue.model';
-export type JiraTransitionOption = 'ALWAYS_ASK' | 'DO_NOT' | JiraOriginalTransition;
+// TODO this needs to be addressed to be either string or JiraOriginalTransition, but not both
+export type JiraTransitionOption =
+ | 'ALWAYS_ASK'
+ | 'DO_NOT'
+ | JiraOriginalTransition
+ | string;
export enum JiraWorklogExportDefaultTime {
AllTime = 'AllTime',
diff --git a/src/app/features/issue/providers/redmine/redmine.const.ts b/src/app/features/issue/providers/redmine/redmine.const.ts
index 5d80daa4107..3ad2523297a 100644
--- a/src/app/features/issue/providers/redmine/redmine.const.ts
+++ b/src/app/features/issue/providers/redmine/redmine.const.ts
@@ -1,11 +1,11 @@
-import {
- ConfigFormSection,
- LimitedFormlyFieldConfig,
-} from 'src/app/features/config/global-config.model';
-import { T } from 'src/app/t.const';
+import { T } from '../../../../t.const';
import { RedmineCfg } from './redmine.model';
import { IssueProviderRedmine } from '../../issue.model';
import { ISSUE_PROVIDER_COMMON_FORM_FIELDS } from '../../common-issue-form-stuff.const';
+import {
+ ConfigFormSection,
+ LimitedFormlyFieldConfig,
+} from '../../../config/global-config.model';
export const REDMINE_POLL_INTERVAL = 5 * 60 * 1000;
export const REDMINE_INITIAL_POLL_DELAY = 8 * 1000;
diff --git a/src/app/features/issue/store/issue-provider-db.effects.ts b/src/app/features/issue/store/issue-provider-db.effects.ts
index 4ee486cbee9..64d8fea960e 100644
--- a/src/app/features/issue/store/issue-provider-db.effects.ts
+++ b/src/app/features/issue/store/issue-provider-db.effects.ts
@@ -1,18 +1,18 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Observable } from 'rxjs';
import { switchMap, take } from 'rxjs/operators';
import { select, Store } from '@ngrx/store';
-import { PersistenceService } from '../../../core/persistence/persistence.service';
import { selectIssueProviderState } from './issue-provider.selectors';
import { IssueProviderActions } from './issue-provider.actions';
import { deleteProject } from '../../project/store/project.actions';
+import { PfapiService } from '../../../pfapi/pfapi.service';
@Injectable()
export class IssueProviderDbEffects {
private _actions$ = inject(Actions);
private _store = inject(Store);
- private _persistenceService = inject(PersistenceService);
+ private _pfapiService = inject(PfapiService);
syncProjectToLs$: Observable = createEffect(
() =>
@@ -35,19 +35,19 @@ export class IssueProviderDbEffects {
IssueProviderActions.clearIssueProviders,
),
- switchMap(() => this.saveToLs$(true)),
+ switchMap(() => this.saveToLs$()),
),
{ dispatch: false },
);
- private saveToLs$(isSyncModelChange: boolean): Observable {
+ private saveToLs$(): Observable {
return this._store.pipe(
// tap(() => console.log('SAVE')),
select(selectIssueProviderState),
take(1),
switchMap((issueProviderState) =>
- this._persistenceService.issueProvider.saveState(issueProviderState, {
- isSyncModelChange,
+ this._pfapiService.m.issueProvider.save(issueProviderState, {
+ isUpdateRevAndLastUpdate: true,
}),
),
);
diff --git a/src/app/features/issue/store/unlink-all-tasks-on-provider-deletion.effects.ts b/src/app/features/issue/store/unlink-all-tasks-on-provider-deletion.effects.ts
index 084cecf407d..e871fd1b2e0 100644
--- a/src/app/features/issue/store/unlink-all-tasks-on-provider-deletion.effects.ts
+++ b/src/app/features/issue/store/unlink-all-tasks-on-provider-deletion.effects.ts
@@ -1,4 +1,4 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { TaskService } from '../../tasks/task.service';
import { TaskCopy } from '../../tasks/task.model';
@@ -8,14 +8,14 @@ import { Observable } from 'rxjs';
import { Update } from '@ngrx/entity/src/models';
import { Store } from '@ngrx/store';
import { __updateMultipleTaskSimple } from '../../tasks/store/task.actions';
-import { PersistenceService } from '../../../core/persistence/persistence.service';
+import { TaskArchiveService } from '../../time-tracking/task-archive.service';
@Injectable()
export class UnlinkAllTasksOnProviderDeletionEffects {
private _actions$ = inject(Actions);
private _taskService = inject(TaskService);
private _store = inject(Store);
- private _persistenceService = inject(PersistenceService);
+ private _taskArchiveService = inject(TaskArchiveService);
readonly UNLINKED_PARTIAL_TASK: Partial = {
issueId: undefined,
@@ -68,10 +68,8 @@ export class UnlinkAllTasksOnProviderDeletionEffects {
changes: this.UNLINKED_PARTIAL_TASK,
};
});
- await this._persistenceService.taskArchive.execAction(
- __updateMultipleTaskSimple({ taskUpdates: archiveTaskUpdates }),
- true,
- );
+
+ await this._taskArchiveService.updateTasks(archiveTaskUpdates);
console.log('unlinkAllTasksOnProviderDeletion$', {
regularTasks,
diff --git a/src/app/features/metric/improvement/store/improvement.effects.ts b/src/app/features/metric/improvement/store/improvement.effects.ts
index 7d366383678..84c481ea9fd 100644
--- a/src/app/features/metric/improvement/store/improvement.effects.ts
+++ b/src/app/features/metric/improvement/store/improvement.effects.ts
@@ -1,4 +1,4 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { filter, first, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { Store } from '@ngrx/store';
@@ -17,17 +17,17 @@ import {
selectImprovementFeatureState,
selectImprovementHideDay,
} from './improvement.reducer';
-import { PersistenceService } from '../../../../core/persistence/persistence.service';
import { selectUnusedImprovementIds } from '../../store/metric.selectors';
import { ImprovementState } from '../improvement.model';
import { DateService } from 'src/app/core/date/date.service';
import { loadAllData } from '../../../../root-store/meta/load-all-data.action';
+import { PfapiService } from '../../../../pfapi/pfapi.service';
@Injectable()
export class ImprovementEffects {
private _actions$ = inject(Actions);
private _store$ = inject>(Store);
- private _persistenceService = inject(PersistenceService);
+ private _pfapiService = inject(PfapiService);
private _dateService = inject(DateService);
updateImprovements$: any = createEffect(
@@ -68,8 +68,8 @@ export class ImprovementEffects {
);
private _saveToLs(improvementState: ImprovementState): void {
- this._persistenceService.improvement.saveState(improvementState, {
- isSyncModelChange: true,
+ this._pfapiService.m.improvement.save(improvementState, {
+ isUpdateRevAndLastUpdate: true,
});
}
}
diff --git a/src/app/features/metric/obstruction/store/obstruction.effects.ts b/src/app/features/metric/obstruction/store/obstruction.effects.ts
index 236902949e7..eb5bb63b056 100644
--- a/src/app/features/metric/obstruction/store/obstruction.effects.ts
+++ b/src/app/features/metric/obstruction/store/obstruction.effects.ts
@@ -1,4 +1,4 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { first, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { select, Store } from '@ngrx/store';
@@ -9,16 +9,16 @@ import {
updateObstruction,
} from './obstruction.actions';
import { selectObstructionFeatureState } from './obstruction.reducer';
-import { PersistenceService } from '../../../../core/persistence/persistence.service';
import { addMetric, updateMetric, upsertMetric } from '../../store/metric.actions';
import { ObstructionState } from '../obstruction.model';
import { selectUnusedObstructionIds } from '../../store/metric.selectors';
+import { PfapiService } from '../../../../pfapi/pfapi.service';
@Injectable()
export class ObstructionEffects {
private _actions$ = inject(Actions);
private _store$ = inject>(Store);
- private _persistenceService = inject(PersistenceService);
+ private _pfapiService = inject(PfapiService);
updateObstructions$: any = createEffect(
() =>
@@ -41,8 +41,8 @@ export class ObstructionEffects {
);
private _saveToLs(obstructionState: ObstructionState): void {
- this._persistenceService.obstruction.saveState(obstructionState, {
- isSyncModelChange: true,
+ this._pfapiService.m.obstruction.save(obstructionState, {
+ isUpdateRevAndLastUpdate: true,
});
}
}
diff --git a/src/app/features/metric/store/metric.effects.ts b/src/app/features/metric/store/metric.effects.ts
index 2cf81c47c8f..c0204ef6286 100644
--- a/src/app/features/metric/store/metric.effects.ts
+++ b/src/app/features/metric/store/metric.effects.ts
@@ -1,17 +1,17 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { first, switchMap, tap } from 'rxjs/operators';
import { select, Store } from '@ngrx/store';
-import { PersistenceService } from '../../../core/persistence/persistence.service';
import { selectMetricFeatureState } from './metric.selectors';
import { MetricState } from '../metric.model';
import { addMetric, deleteMetric, updateMetric, upsertMetric } from './metric.actions';
+import { PfapiService } from '../../../pfapi/pfapi.service';
@Injectable()
export class MetricEffects {
private _actions$ = inject(Actions);
private _store$ = inject>(Store);
- private _persistenceService = inject(PersistenceService);
+ private _pfapiService = inject(PfapiService);
updateMetrics$: any = createEffect(
() =>
@@ -26,6 +26,8 @@ export class MetricEffects {
);
private _saveToLs(metricState: MetricState): void {
- this._persistenceService.metric.saveState(metricState, { isSyncModelChange: true });
+ this._pfapiService.m.metric.save(metricState, {
+ isUpdateRevAndLastUpdate: true,
+ });
}
}
diff --git a/src/app/features/note/note.service.ts b/src/app/features/note/note.service.ts
index 6e1d5b50a36..88dcff5ecd0 100644
--- a/src/app/features/note/note.service.ts
+++ b/src/app/features/note/note.service.ts
@@ -15,20 +15,20 @@ import {
selectNoteById,
selectNoteFeatureState,
} from './store/note.reducer';
-import { PersistenceService } from '../../core/persistence/persistence.service';
import { take } from 'rxjs/operators';
import { createFromDrop } from '../../core/drop-paste-input/drop-paste-input';
import { isImageUrl, isImageUrlSimple } from '../../util/is-image-url';
import { DropPasteInput } from '../../core/drop-paste-input/drop-paste.model';
import { WorkContextService } from '../work-context/work-context.service';
import { WorkContextType } from '../work-context/work-context.model';
+import { PfapiService } from '../../pfapi/pfapi.service';
@Injectable({
providedIn: 'root',
})
export class NoteService {
private _store$ = inject>(Store);
- private _persistenceService = inject(PersistenceService);
+ private _pfapiService = inject(PfapiService);
private _workContextService = inject(WorkContextService);
notes$: Observable = this._store$.pipe(select(selectAllNotes));
diff --git a/src/app/features/note/note/note.component.ts b/src/app/features/note/note/note.component.ts
index 7525aadbbd7..194ed5133fb 100644
--- a/src/app/features/note/note/note.component.ts
+++ b/src/app/features/note/note/note.component.ts
@@ -33,6 +33,7 @@ import {
} from '@angular/material/menu';
import { AsyncPipe } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
+import { DEFAULT_PROJECT_COLOR } from '../../work-context/work-context.const';
@Component({
selector: 'note',
@@ -93,7 +94,11 @@ export class NoteComponent implements OnChanges {
(project) =>
project && {
...project,
+ color: project.theme.primary || DEFAULT_PROJECT_COLOR,
icon: 'list',
+ theme: {
+ primary: project.theme.primary || DEFAULT_PROJECT_COLOR,
+ },
},
),
)
diff --git a/src/app/features/note/store/note.effects.ts b/src/app/features/note/store/note.effects.ts
index a0338e1aad9..c66b6a3c5fc 100644
--- a/src/app/features/note/store/note.effects.ts
+++ b/src/app/features/note/store/note.effects.ts
@@ -1,6 +1,5 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
-import { PersistenceService } from '../../../core/persistence/persistence.service';
import { select, Store } from '@ngrx/store';
import { first, switchMap, tap } from 'rxjs/operators';
import {
@@ -11,17 +10,16 @@ import {
updateNoteOrder,
} from './note.actions';
import { selectNoteFeatureState } from './note.reducer';
-import { WorkContextService } from '../../work-context/work-context.service';
import { Observable } from 'rxjs';
import { NoteState } from '../note.model';
import { deleteProject } from '../../project/store/project.actions';
+import { PfapiService } from '../../../pfapi/pfapi.service';
@Injectable()
export class NoteEffects {
private _actions$ = inject(Actions);
private _store$ = inject>(Store);
- private _persistenceService = inject(PersistenceService);
- private _workContextService = inject(WorkContextService);
+ private _pfapiService = inject(PfapiService);
updateNote$: Observable = createEffect(
() =>
@@ -42,8 +40,8 @@ export class NoteEffects {
);
private _saveToLs(noteState: NoteState): void {
- this._persistenceService.note.saveState(noteState, {
- isSyncModelChange: true,
+ this._pfapiService.m.note.save(noteState, {
+ isUpdateRevAndLastUpdate: true,
});
}
}
diff --git a/src/app/features/planner/store/planner.effects.ts b/src/app/features/planner/store/planner.effects.ts
index d230ec95b5b..802f45b45d6 100644
--- a/src/app/features/planner/store/planner.effects.ts
+++ b/src/app/features/planner/store/planner.effects.ts
@@ -1,8 +1,7 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { PlannerActions } from './planner.actions';
import { filter, map, tap, withLatestFrom } from 'rxjs/operators';
-import { PersistenceService } from '../../../core/persistence/persistence.service';
import { select, Store } from '@ngrx/store';
import { selectPlannerState } from './planner.selectors';
import { PlannerState } from './planner.reducer';
@@ -11,12 +10,13 @@ import {
unScheduleTask,
updateTaskTags,
} from '../../tasks/store/task.actions';
+import { PfapiService } from '../../../pfapi/pfapi.service';
@Injectable()
export class PlannerEffects {
private _actions$ = inject(Actions);
private _store = inject(Store);
- private _persistenceService = inject(PersistenceService);
+ private _pfapiService = inject(PfapiService);
saveToDB$ = createEffect(
() => {
@@ -33,7 +33,7 @@ export class PlannerEffects {
updateTaskTags,
),
withLatestFrom(this._store.pipe(select(selectPlannerState))),
- tap(([, plannerState]) => this._saveToLs(plannerState, true)),
+ tap(([, plannerState]) => this._saveToLs(plannerState)),
);
},
{ dispatch: false },
@@ -67,12 +67,9 @@ export class PlannerEffects {
);
});
- private _saveToLs(
- plannerState: PlannerState,
- isSyncModelChange: boolean = false,
- ): void {
- this._persistenceService.planner.saveState(plannerState, {
- isSyncModelChange,
+ private _saveToLs(plannerState: PlannerState): void {
+ this._pfapiService.m.planner.save(plannerState, {
+ isUpdateRevAndLastUpdate: true,
});
}
}
diff --git a/src/app/features/pomodoro/store/pomodoro.effects.spec.ts b/src/app/features/pomodoro/store/pomodoro.effects.spec.ts
index bab34bb94d7..ad371c7af25 100644
--- a/src/app/features/pomodoro/store/pomodoro.effects.spec.ts
+++ b/src/app/features/pomodoro/store/pomodoro.effects.spec.ts
@@ -33,7 +33,7 @@ describe('PomodoroEffects', () => {
isEnabled$ = new BehaviorSubject(true);
currentSessionTime$ = new BehaviorSubject(5000);
isBreak$ = new BehaviorSubject(false);
- allStartableTasks$ = new BehaviorSubject([]);
+ allStartableTasks$ = new BehaviorSubject([]);
TestBed.configureTestingModule({
providers: [
PomodoroEffects,
diff --git a/src/app/features/project/migrate-projects-state.util.ts b/src/app/features/project/migrate-projects-state.util.ts
index a12ef2695d6..ad1495c0deb 100644
--- a/src/app/features/project/migrate-projects-state.util.ts
+++ b/src/app/features/project/migrate-projects-state.util.ts
@@ -6,7 +6,6 @@ import { isMigrateModel } from '../../util/is-migrate-model';
import { WORK_CONTEXT_DEFAULT_THEME } from '../work-context/work-context.const';
import { dirtyDeepCopy } from '../../util/dirtyDeepCopy';
import { MODEL_VERSION } from '../../core/model-version';
-import { roundTsToMinutes } from '../../util/round-ts-to-minutes';
export const migrateProjectState = (projectState: ProjectState): ProjectState => {
if (!isMigrateModel(projectState, MODEL_VERSION.PROJECT, 'Project')) {
@@ -16,7 +15,6 @@ export const migrateProjectState = (projectState: ProjectState): ProjectState =>
const projectEntities: Dictionary = { ...projectState.entities };
Object.keys(projectEntities).forEach((key) => {
projectEntities[key] = _updateThemeModel(projectEntities[key] as Project);
- projectEntities[key] = _reduceWorkStartEndPrecision(projectEntities[key] as Project);
// NOTE: absolutely needs to come last as otherwise the previous defaults won't work
projectEntities[key] = _extendProjectDefaults(projectEntities[key] as Project);
@@ -56,29 +54,6 @@ const _removeOutdatedData = (project: Project): Project => {
return copy;
};
-const __reduceWorkStartEndPrecision = (workStartEnd: {
- [key: string]: any;
-}): {
- [key: string]: any;
-} => {
- return workStartEnd
- ? Object.keys(workStartEnd).reduce((acc, dateKey) => {
- return {
- ...acc,
- [dateKey]: roundTsToMinutes(workStartEnd[dateKey]),
- };
- }, {})
- : workStartEnd;
-};
-
-const _reduceWorkStartEndPrecision = (project: Project): Project => {
- return {
- ...project,
- workStart: __reduceWorkStartEndPrecision(project.workStart),
- workEnd: __reduceWorkStartEndPrecision(project.workEnd),
- };
-};
-
const _updateThemeModel = (project: Project): Project => {
return project.hasOwnProperty('theme') && project.theme.primary
? project
diff --git a/src/app/features/project/project.const.ts b/src/app/features/project/project.const.ts
index 90a69d2e5a2..2c8a0ecf9ad 100644
--- a/src/app/features/project/project.const.ts
+++ b/src/app/features/project/project.const.ts
@@ -25,6 +25,4 @@ export const FIRST_PROJECT: Project = {
id: DEFAULT_PROJECT_ID,
title: 'Inbox',
icon: 'inbox',
- workStart: {},
- workEnd: {},
};
diff --git a/src/app/features/project/project.model.ts b/src/app/features/project/project.model.ts
index 508857e5c86..7fc0274b1be 100644
--- a/src/app/features/project/project.model.ts
+++ b/src/app/features/project/project.model.ts
@@ -11,6 +11,7 @@ export type RoundTimeOption = '5M' | 'QUARTER' | 'HALF' | 'HOUR' | null;
export interface ProjectBasicCfg {
title: string;
isHiddenFromMenu: boolean;
+ // TODO remove maybe
isArchived: boolean;
isEnableBacklog: boolean;
taskIds: string[];
@@ -21,7 +22,7 @@ export interface ProjectBasicCfg {
export interface ProjectCopy extends ProjectBasicCfg, WorkContextCommon {
id: string;
// TODO legacy remove
- issueIntegrationCfgs?: IssueIntegrationCfgs;
+ issueIntegrationCfgs?: IssueIntegrationCfgs | { [key: string]: any };
}
export type Project = Readonly;
diff --git a/src/app/features/project/project.service.ts b/src/app/features/project/project.service.ts
index 6c917ff8f6a..87125afa333 100644
--- a/src/app/features/project/project.service.ts
+++ b/src/app/features/project/project.service.ts
@@ -4,8 +4,14 @@ import { Project } from './project.model';
import { select, Store } from '@ngrx/store';
import { nanoid } from 'nanoid';
import { Actions, ofType } from '@ngrx/effects';
-import { catchError, shareReplay, switchMap, take } from 'rxjs/operators';
-import { BreakNr, BreakTime, WorkContextType } from '../work-context/work-context.model';
+import { catchError, map, shareReplay, switchMap, take } from 'rxjs/operators';
+import {
+ BreakNr,
+ BreakNrCopy,
+ BreakTime,
+ BreakTimeCopy,
+ WorkContextType,
+} from '../work-context/work-context.model';
import { WorkContextService } from '../work-context/work-context.service';
import {
addProject,
@@ -22,8 +28,6 @@ import {
import { DEFAULT_PROJECT } from './project.const';
import {
selectArchivedProjects,
- selectProjectBreakNrForProject,
- selectProjectBreakTimeForProject,
selectProjectById,
selectUnarchivedProjects,
selectUnarchivedProjectsWithoutCurrent,
@@ -31,6 +35,7 @@ import {
import { devError } from '../../util/dev-error';
import { selectTaskFeatureState } from '../tasks/store/task.selectors';
import { getTaskById } from '../tasks/store/task.reducer.util';
+import { TimeTrackingService } from '../time-tracking/time-tracking.service';
@Injectable({
providedIn: 'root',
@@ -39,6 +44,7 @@ export class ProjectService {
private readonly _workContextService = inject(WorkContextService);
private readonly _store$ = inject>(Store);
private readonly _actions$ = inject(Actions);
+ private readonly _timeTrackingService = inject(TimeTrackingService);
list$: Observable = this._store$.pipe(select(selectUnarchivedProjects));
@@ -63,11 +69,37 @@ export class ProjectService {
}
getBreakNrForProject$(projectId: string): Observable {
- return this._store$.pipe(select(selectProjectBreakNrForProject, { id: projectId }));
+ return this._timeTrackingService.state$.pipe(
+ map((current) => {
+ const dataForProject = current.project[projectId];
+ const breakNr: BreakNrCopy = {};
+ Object.keys(dataForProject).forEach((dateStr) => {
+ const dateData = dataForProject[dateStr];
+ if (typeof dateData?.b === 'number') {
+ breakNr[dateStr] = dateData.b;
+ }
+ });
+ return breakNr;
+ }),
+ );
+
+ // return this._store$.pipe(select(selectProjectBreakNrForProject, { id: projectId }));
}
getBreakTimeForProject$(projectId: string): Observable {
- return this._store$.pipe(select(selectProjectBreakTimeForProject, { id: projectId }));
+ return this._timeTrackingService.state$.pipe(
+ map((current) => {
+ const dataForProject = current.project[projectId];
+ const breakTime: BreakTimeCopy = {};
+ Object.keys(dataForProject).forEach((dateStr) => {
+ const dateData = dataForProject[dateStr];
+ if (typeof dateData?.bt === 'number') {
+ breakTime[dateStr] = dateData.bt;
+ }
+ });
+ return breakTime;
+ }),
+ );
}
archive(projectId: string): void {
diff --git a/src/app/features/project/store/project.actions.ts b/src/app/features/project/store/project.actions.ts
index 48d48f4d884..809f53d9c67 100644
--- a/src/app/features/project/store/project.actions.ts
+++ b/src/app/features/project/store/project.actions.ts
@@ -34,21 +34,6 @@ export const updateProject = createAction(
props<{ project: Update }>(),
);
-export const updateProjectWorkStart = createAction(
- '[Project] Update Work Start',
- props<{ id: string; date: string; newVal: number }>(),
-);
-
-export const updateProjectWorkEnd = createAction(
- '[Project] Update Work End',
- props<{ id: string; date: string; newVal: number }>(),
-);
-
-export const addToProjectBreakTime = createAction(
- '[Project] Add to Break Time',
- props<{ id: string; date: string; valToAdd: number }>(),
-);
-
export const updateProjectAdvancedCfg = createAction(
'[Project] Update Project Advanced Cfg',
props<{
diff --git a/src/app/features/project/store/project.effects.ts b/src/app/features/project/store/project.effects.ts
index 40d434c6a47..03fd0a0ca79 100644
--- a/src/app/features/project/store/project.effects.ts
+++ b/src/app/features/project/store/project.effects.ts
@@ -1,11 +1,10 @@
import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { select, Store } from '@ngrx/store';
-import { concatMap, filter, first, map, switchMap, take, tap } from 'rxjs/operators';
+import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import {
addProject,
addProjects,
- addToProjectBreakTime,
archiveProject,
deleteProject,
moveAllProjectBacklogTasksToRegularList,
@@ -22,23 +21,17 @@ import {
updateProject,
updateProjectAdvancedCfg,
updateProjectOrder,
- updateProjectWorkEnd,
- updateProjectWorkStart,
upsertProject,
} from './project.actions';
-import { PersistenceService } from '../../../core/persistence/persistence.service';
import { SnackService } from '../../../core/snack/snack.service';
import {
addTask,
- addTimeSpent,
convertToMainTask,
deleteTask,
- deleteTasks,
moveToArchive_,
moveToOtherProject,
restoreTask,
} from '../../tasks/store/task.actions';
-import { ProjectService } from '../project.service';
import { GlobalConfigService } from '../../config/global-config.service';
import { T } from '../../../t.const';
import {
@@ -48,8 +41,6 @@ import {
} from '../../work-context/store/work-context-meta.actions';
import { WorkContextType } from '../../work-context/work-context.model';
import { Project } from '../project.model';
-import { Task, TaskArchive } from '../../tasks/task.model';
-import { unique } from '../../../util/unique';
import { EMPTY, Observable, of } from 'rxjs';
import { selectProjectFeatureState } from './project.selectors';
import {
@@ -58,19 +49,21 @@ import {
moveNoteToOtherProject,
updateNoteOrder,
} from '../../note/store/note.actions';
-import { DateService } from 'src/app/core/date/date.service';
import { ReminderService } from '../../reminder/reminder.service';
+import { PfapiService } from '../../../pfapi/pfapi.service';
+import { TaskArchiveService } from '../../time-tracking/task-archive.service';
+import { TimeTrackingService } from '../../time-tracking/time-tracking.service';
@Injectable()
export class ProjectEffects {
private _actions$ = inject(Actions);
private _store$ = inject>(Store);
private _snackService = inject(SnackService);
- private _projectService = inject(ProjectService);
- private _persistenceService = inject(PersistenceService);
+ private _taskArchiveService = inject(TaskArchiveService);
+ private _pfapiService = inject(PfapiService);
private _globalConfigService = inject(GlobalConfigService);
- private _dateService = inject(DateService);
private _reminderService = inject(ReminderService);
+ private _timeTrackingService = inject(TimeTrackingService);
syncProjectToLs$: Observable = createEffect(
() =>
@@ -82,9 +75,6 @@ export class ProjectEffects {
deleteProject.type,
updateProject.type,
updateProjectAdvancedCfg.type,
- updateProjectWorkStart.type,
- updateProjectWorkEnd.type,
- addToProjectBreakTime.type,
updateProjectOrder.type,
archiveProject.type,
unarchiveProject.type,
@@ -102,16 +92,7 @@ export class ProjectEffects {
moveProjectTaskToRegularListAuto.type,
),
switchMap((a) => {
- // exclude ui only actions
- if (
- [updateProjectWorkStart.type, updateProjectWorkEnd.type].includes(
- a.type as any,
- )
- ) {
- return this.saveToLs$(false);
- } else {
- return this.saveToLs$(true);
- }
+ return this.saveToLs$(true);
}),
),
{ dispatch: false },
@@ -191,38 +172,6 @@ export class ProjectEffects {
{ dispatch: false },
);
- updateWorkStart$: any = createEffect(() =>
- this._actions$.pipe(
- ofType(addTimeSpent),
- filter(({ task }) => !!task.projectId),
- concatMap(({ task }) =>
- this._projectService.getByIdOnce$(task.projectId as string).pipe(first()),
- ),
- filter((project: Project) => !project.workStart[this._dateService.todayStr()]),
- map((project) => {
- return updateProjectWorkStart({
- id: project.id,
- date: this._dateService.todayStr(),
- newVal: Date.now(),
- });
- }),
- ),
- );
-
- updateWorkEnd$: Observable = createEffect(() =>
- this._actions$.pipe(
- ofType(addTimeSpent),
- filter(({ task }) => !!task.projectId),
- map(({ task }) => {
- return updateProjectWorkEnd({
- id: task.projectId as string,
- date: this._dateService.todayStr(),
- newVal: Date.now(),
- });
- }),
- ),
- );
-
// TODO a solution for orphaned tasks might be needed
deleteProjectRelatedData: Observable = createEffect(
@@ -232,8 +181,9 @@ export class ProjectEffects {
tap(async ({ project, allTaskIds }) => {
// NOTE: we also do stuff on a reducer level (probably better to handle on this level @TODO refactor)
const id = project.id as string;
- this._removeAllArchiveTasksForProject(id);
+ this._taskArchiveService.removeAllArchiveTasksForProject(id);
this._reminderService.removeRemindersByRelatedIds(allTaskIds);
+ this._timeTrackingService.cleanupDataEverywhereForProject(id);
// we also might need to account for this unlikely but very nasty scenario
const cfg = await this._globalConfigService.cfg$.pipe(take(1)).toPromise();
@@ -264,7 +214,7 @@ export class ProjectEffects {
// this._actions$.pipe(
// ofType(archiveProject.type),
// tap(async ({ id }) => {
- // await this._persistenceService.archiveProject(id);
+ // await this._pfapiService.archiveProject(id);
// // TODO remove reminders
// this._snackService.open({
// ico: 'archive',
@@ -279,7 +229,7 @@ export class ProjectEffects {
// this._actions$.pipe(
// ofType(unarchiveProject.type),
// tap(async ({ id }) => {
- // await this._persistenceService.unarchiveProject(id);
+ // await this._pfapiService.unarchiveProject(id);
//
// this._snackService.open({
// ico: 'unarchive',
@@ -337,40 +287,15 @@ export class ProjectEffects {
{ dispatch: false },
);
- private async _removeAllArchiveTasksForProject(
- projectIdToDelete: string,
- ): Promise {
- const taskArchiveState: TaskArchive =
- await this._persistenceService.taskArchive.loadState();
- // NOTE: task archive might not if there never was a day completed
- const archiveTaskIdsToDelete = !!taskArchiveState
- ? (taskArchiveState.ids as string[]).filter((id) => {
- const t = taskArchiveState.entities[id] as Task;
- if (!t) {
- throw new Error('No task');
- }
- return t.projectId === projectIdToDelete;
- })
- : [];
- console.log(
- 'Archive TaskIds to remove/unique',
- archiveTaskIdsToDelete,
- unique(archiveTaskIdsToDelete),
- );
- // remove archive
- await this._persistenceService.taskArchive.execAction(
- deleteTasks({ taskIds: archiveTaskIdsToDelete }),
- true,
- );
- }
-
- private saveToLs$(isSyncModelChange: boolean): Observable {
+ private saveToLs$(isUpdateRevAndLastUpdate: boolean): Observable {
return this._store$.pipe(
// tap(() => console.log('SAVE')),
select(selectProjectFeatureState),
take(1),
switchMap((projectState) =>
- this._persistenceService.project.saveState(projectState, { isSyncModelChange }),
+ this._pfapiService.m.project.save(projectState, {
+ isUpdateRevAndLastUpdate,
+ }),
),
);
}
diff --git a/src/app/features/project/store/project.reducer.ts b/src/app/features/project/store/project.reducer.ts
index e3a6519429c..69c70a95916 100644
--- a/src/app/features/project/store/project.reducer.ts
+++ b/src/app/features/project/store/project.reducer.ts
@@ -42,7 +42,6 @@ import { devError } from '../../../util/dev-error';
import {
addProject,
addProjects,
- addToProjectBreakTime,
archiveProject,
deleteProject,
loadProjects,
@@ -61,8 +60,6 @@ import {
updateProject,
updateProjectAdvancedCfg,
updateProjectOrder,
- updateProjectWorkEnd,
- updateProjectWorkStart,
upsertProject,
} from './project.actions';
import {
@@ -72,7 +69,6 @@ import {
updateNoteOrder,
} from '../../note/store/note.actions';
import { MODEL_VERSION } from '../../../core/model-version';
-import { roundTsToMinutes } from '../../../util/round-ts-to-minutes';
export const PROJECT_FEATURE_NAME = 'projects';
const WORK_CONTEXT_TYPE: WorkContextType = WorkContextType.PROJECT;
@@ -193,60 +189,6 @@ export const projectReducer = createReducer(
),
),
- on(updateProjectWorkStart, (state, { id, date, newVal }) => {
- const oldP = state.entities[id] as Project;
- return projectAdapter.updateOne(
- {
- id,
- changes: {
- workStart: {
- ...oldP.workStart,
- [date]: roundTsToMinutes(newVal),
- },
- },
- },
- state,
- );
- }),
- on(updateProjectWorkEnd, (state, { id, date, newVal }) => {
- const oldP = state.entities[id] as Project;
- return projectAdapter.updateOne(
- {
- id,
- changes: {
- workEnd: {
- ...oldP.workEnd,
- [date]: roundTsToMinutes(newVal),
- },
- },
- },
- state,
- );
- }),
-
- on(addToProjectBreakTime, (state, { id, date, valToAdd }) => {
- const oldP = state.entities[id] as Project;
- const oldBreakTime = oldP.breakTime[date] || 0;
- const oldBreakNr = oldP.breakNr[date] || 0;
-
- return projectAdapter.updateOne(
- {
- id,
- changes: {
- breakNr: {
- ...oldP.breakNr,
- [date]: oldBreakNr + 1,
- },
- breakTime: {
- ...oldP.breakTime,
- [date]: oldBreakTime + valToAdd,
- },
- },
- },
- state,
- );
- }),
-
on(updateProjectAdvancedCfg, (state, { projectId, sectionKey, data }) => {
const currentProject = state.entities[projectId] as Project;
const advancedCfg: WorkContextAdvancedCfg = Object.assign(
diff --git a/src/app/features/reminder/reminder.service.ts b/src/app/features/reminder/reminder.service.ts
index 9ac0d91b373..4f2f7b02c90 100644
--- a/src/app/features/reminder/reminder.service.ts
+++ b/src/app/features/reminder/reminder.service.ts
@@ -1,11 +1,10 @@
import { nanoid } from 'nanoid';
-import { Injectable, inject } from '@angular/core';
-import { PersistenceService } from '../../core/persistence/persistence.service';
+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 { ImexMetaService } from '../../imex/imex-meta/imex-meta.service';
+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';
@@ -15,16 +14,17 @@ import { migrateReminders } from './migrate-reminder.util';
import { devError } from '../../util/dev-error';
import { Note } from '../note/note.model';
import { environment } from 'src/environments/environment';
+import { PfapiService } from '../../pfapi/pfapi.service';
@Injectable({
providedIn: 'root',
})
export class ReminderService {
- private readonly _persistenceService = inject(PersistenceService);
+ private readonly _pfapiService = inject(PfapiService);
private readonly _snackService = inject(SnackService);
private readonly _taskService = inject(TaskService);
private readonly _noteService = inject(NoteService);
- private readonly _imexMetaService = inject(ImexMetaService);
+ private readonly _imexMetaService = inject(ImexViewService);
private _onRemindersActive$: Subject = new Subject();
onRemindersActive$: Observable = this._onRemindersActive$.pipe(
@@ -224,7 +224,7 @@ export class ReminderService {
}
private async _loadFromDatabase(): Promise {
- return migrateReminders((await this._persistenceService.reminders.loadState()) || []);
+ return migrateReminders((await this._pfapiService.m.reminders.load()) || []);
}
private async _saveModel(reminders: Reminder[]): Promise {
@@ -232,8 +232,8 @@ export class ReminderService {
throw new Error('Reminders not loaded initially when trying to save model');
}
console.log('saveReminders', reminders);
- await this._persistenceService.reminders.saveState(reminders, {
- isSyncModelChange: true,
+ await this._pfapiService.m.reminders.save(reminders, {
+ isUpdateRevAndLastUpdate: true,
});
this._updateRemindersInWorker(this._reminders);
this._reminders$.next(this._reminders);
diff --git a/src/app/features/reminder/store/reminder-countdown.effects.ts b/src/app/features/reminder/store/reminder-countdown.effects.ts
index d156f1838fb..dddd1321a50 100644
--- a/src/app/features/reminder/store/reminder-countdown.effects.ts
+++ b/src/app/features/reminder/store/reminder-countdown.effects.ts
@@ -22,11 +22,11 @@ import { BannerService } from '../../../core/banner/banner.service';
import { Reminder } from '../reminder.model';
import { selectReminderConfig } from '../../config/store/global-config.reducer';
import { BehaviorSubject, combineLatest, EMPTY, timer } from 'rxjs';
-import { DataInitService } from '../../../core/data-init/data-init.service';
import { TaskService } from '../../tasks/task.service';
import { Task, TaskWithReminder } from '../../tasks/task.model';
import { ProjectService } from '../../project/project.service';
import { Router } from '@angular/router';
+import { DataInitStateService } from '../../../core/data-init/data-init-state.service';
const UPDATE_PERCENTAGE_INTERVAL = 250;
// since the reminder modal doesn't show instantly we adjust a little for that
@@ -39,14 +39,14 @@ export class ReminderCountdownEffects {
private _datePipe = inject(DatePipe);
private _store = inject(Store);
private _bannerService = inject(BannerService);
- private _dataInitService = inject(DataInitService);
+ private _dataInitStateService = inject(DataInitStateService);
private _taskService = inject(TaskService);
private _projectService = inject(ProjectService);
private _router = inject(Router);
reminderCountdownBanner$ = createEffect(
() =>
- this._dataInitService.isAllDataLoadedInitially$.pipe(
+ this._dataInitStateService.isAllDataLoadedInitially$.pipe(
concatMap(() => this._store.select(selectReminderConfig)),
switchMap((reminderCfg) =>
reminderCfg.isCountdownBannerEnabled
diff --git a/src/app/features/search-bar/search-bar.component.ts b/src/app/features/search-bar/search-bar.component.ts
index ca1df98b9e0..ea5f5cfd501 100644
--- a/src/app/features/search-bar/search-bar.component.ts
+++ b/src/app/features/search-bar/search-bar.component.ts
@@ -117,7 +117,7 @@ export class SearchBarComponent implements AfterViewInit, OnDestroy {
return {
id: task.id,
title: task.title.toLowerCase(),
- taskNotes: task.notes.toLowerCase(),
+ taskNotes: task.notes?.toLowerCase() || '',
projectId: task.projectId || null,
parentId: task.parentId || null,
tagId,
diff --git a/src/app/features/shepherd/shepherd.component.ts b/src/app/features/shepherd/shepherd.component.ts
index 87b7c1c73d2..dd109c0104e 100644
--- a/src/app/features/shepherd/shepherd.component.ts
+++ b/src/app/features/shepherd/shepherd.component.ts
@@ -2,8 +2,8 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, inject } from '@angu
import { ShepherdService } from './shepherd.service';
import { LS } from '../../core/persistence/storage-keys.const';
import { concatMap, first } from 'rxjs/operators';
-import { DataInitService } from '../../core/data-init/data-init.service';
import { ProjectService } from '../project/project.service';
+import { DataInitStateService } from '../../core/data-init/data-init-state.service';
@Component({
selector: 'shepherd',
@@ -14,12 +14,12 @@ import { ProjectService } from '../project/project.service';
})
export class ShepherdComponent implements AfterViewInit {
private shepherdMyService = inject(ShepherdService);
- private _dataInitService = inject(DataInitService);
+ private _dataInitStateService = inject(DataInitStateService);
private _projectService = inject(ProjectService);
ngAfterViewInit(): void {
if (!localStorage.getItem(LS.IS_SHOW_TOUR) && navigator.userAgent !== 'NIGHTWATCH') {
- this._dataInitService.isAllDataLoadedInitially$
+ this._dataInitStateService.isAllDataLoadedInitially$
.pipe(concatMap(() => this._projectService.list$.pipe(first())))
.subscribe((projectList) => {
if (projectList.length <= 2) {
diff --git a/src/app/features/simple-counter/get-simple-counter-streak-duration.spec.ts b/src/app/features/simple-counter/get-simple-counter-streak-duration.spec.ts
index 70f355d44e9..541c2a41b45 100644
--- a/src/app/features/simple-counter/get-simple-counter-streak-duration.spec.ts
+++ b/src/app/features/simple-counter/get-simple-counter-streak-duration.spec.ts
@@ -43,9 +43,9 @@ describe('getSimpleCounterStreakDuration()', () => {
},
},
];
- T1.forEach((sc: SimpleCounterCopy) => {
+ T1.forEach((sc: Partial) => {
it('should return 0 if no streak', () => {
- expect(getSimpleCounterStreakDuration(sc)).toBe(0);
+ expect(getSimpleCounterStreakDuration(sc as SimpleCounterCopy)).toBe(0);
});
});
@@ -83,9 +83,9 @@ describe('getSimpleCounterStreakDuration()', () => {
},
];
- T2.forEach((sc: SimpleCounterCopy) => {
+ T2.forEach((sc: Partial) => {
it('should return 1 if streak', () => {
- expect(getSimpleCounterStreakDuration(sc)).toBe(1);
+ expect(getSimpleCounterStreakDuration(sc as SimpleCounterCopy)).toBe(1);
});
});
@@ -124,9 +124,9 @@ describe('getSimpleCounterStreakDuration()', () => {
},
];
- T3.forEach((sc: SimpleCounterCopy) => {
+ T3.forEach((sc: Partial) => {
it('should return 2 if streak', () => {
- expect(getSimpleCounterStreakDuration(sc)).toBe(2);
+ expect(getSimpleCounterStreakDuration(sc as SimpleCounterCopy)).toBe(2);
});
});
@@ -197,9 +197,9 @@ describe('getSimpleCounterStreakDuration()', () => {
},
];
- T4.forEach((sc: SimpleCounterCopy) => {
+ T4.forEach((sc: Partial) => {
it('should return 14 if streak', () => {
- expect(getSimpleCounterStreakDuration(sc)).toBe(14);
+ expect(getSimpleCounterStreakDuration(sc as SimpleCounterCopy)).toBe(14);
});
});
@@ -269,9 +269,9 @@ describe('getSimpleCounterStreakDuration()', () => {
},
];
- T5.forEach((sc: SimpleCounterCopy) => {
+ T5.forEach((sc: Partial) => {
it('should start counting at yesterday not today', () => {
- expect(getSimpleCounterStreakDuration(sc)).toBe(13);
+ expect(getSimpleCounterStreakDuration(sc as SimpleCounterCopy)).toBe(13);
});
});
});
diff --git a/src/app/features/simple-counter/store/simple-counter.effects.ts b/src/app/features/simple-counter/store/simple-counter.effects.ts
index ef320be14d2..96f41b77d29 100644
--- a/src/app/features/simple-counter/store/simple-counter.effects.ts
+++ b/src/app/features/simple-counter/store/simple-counter.effects.ts
@@ -2,7 +2,6 @@ import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { select, Store } from '@ngrx/store';
import confetti from 'canvas-confetti';
-import { PersistenceService } from '../../../core/persistence/persistence.service';
import {
addSimpleCounter,
deleteSimpleCounter,
@@ -30,6 +29,7 @@ import { DateService } from 'src/app/core/date/date.service';
import { getWorklogStr } from '../../../util/get-work-log-str';
import { getSimpleCounterStreakDuration } from '../get-simple-counter-streak-duration';
import { TranslateService } from '@ngx-translate/core';
+import { PfapiService } from '../../../pfapi/pfapi.service';
@Injectable()
export class SimpleCounterEffects {
@@ -37,7 +37,7 @@ export class SimpleCounterEffects {
private _store$ = inject>(Store);
private _timeTrackingService = inject(GlobalTrackingIntervalService);
private _dateService = inject(DateService);
- private _persistenceService = inject(PersistenceService);
+ private _pfapiService = inject(PfapiService);
private _simpleCounterService = inject(SimpleCounterService);
private _snackService = inject(SnackService);
private _translateService = inject(TranslateService);
@@ -155,8 +155,8 @@ export class SimpleCounterEffects {
);
private _saveToLs(simpleCounterState: SimpleCounterState): void {
- this._persistenceService.simpleCounter.saveState(simpleCounterState, {
- isSyncModelChange: true,
+ this._pfapiService.m.simpleCounter.save(simpleCounterState, {
+ isUpdateRevAndLastUpdate: true,
});
}
diff --git a/src/app/features/tag/store/tag.actions.ts b/src/app/features/tag/store/tag.actions.ts
index 483d4d2d8b0..f488af56865 100644
--- a/src/app/features/tag/store/tag.actions.ts
+++ b/src/app/features/tag/store/tag.actions.ts
@@ -29,21 +29,6 @@ export const updateAdvancedConfigForTag = createAction(
props<{ tagId: string; sectionKey: WorkContextAdvancedCfgKey; data: any }>(),
);
-export const updateWorkStartForTag = createAction(
- '[Tag] Update Work Start for Tag',
- props<{ id: string; date: string; newVal: number }>(),
-);
-
-export const updateWorkEndForTag = createAction(
- '[Tag] Update Work End for Tag',
- props<{ id: string; date: string; newVal: number }>(),
-);
-
-export const addToBreakTimeForTag = createAction(
- '[Tag] Update Break Time for Tag',
- props<{ id: string; date: string; valToAdd: number }>(),
-);
-
export const moveTaskInTagList = createAction(
'[Tag] Switch places of taskIds in tagList',
props<{ tagId: string; fromTaskId: string; toTaskId: string }>(),
diff --git a/src/app/features/tag/store/tag.effects.ts b/src/app/features/tag/store/tag.effects.ts
index 8d8e4bd0c85..8fd1aa35e0b 100644
--- a/src/app/features/tag/store/tag.effects.ts
+++ b/src/app/features/tag/store/tag.effects.ts
@@ -1,7 +1,6 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import {
- concatMap,
filter,
first,
map,
@@ -13,74 +12,69 @@ import {
} from 'rxjs/operators';
import { select, Store } from '@ngrx/store';
import { selectTagById, selectTagFeatureState } from './tag.reducer';
-import { PersistenceService } from '../../../core/persistence/persistence.service';
import { T } from '../../../t.const';
import { SnackService } from '../../../core/snack/snack.service';
import {
addTag,
- addToBreakTimeForTag,
deleteTag,
deleteTags,
moveTaskInTagList,
updateAdvancedConfigForTag,
updateTag,
updateTagOrder,
- updateWorkEndForTag,
- updateWorkStartForTag,
upsertTag,
} from './tag.actions';
import {
addTask,
- addTimeSpent,
convertToMainTask,
deleteTask,
deleteTasks,
moveToArchive_,
moveToOtherProject,
- removeTagsForAllTasks,
restoreTask,
updateTaskTags,
} from '../../tasks/store/task.actions';
import { TagService } from '../tag.service';
import { TaskService } from '../../tasks/task.service';
import { EMPTY, Observable, of } from 'rxjs';
-import { Task, TaskArchive } from '../../tasks/task.model';
-import { Tag } from '../tag.model';
+import { Task } from '../../tasks/task.model';
import { WorkContextType } from '../../work-context/work-context.model';
import { WorkContextService } from '../../work-context/work-context.service';
import { Router } from '@angular/router';
import { NO_LIST_TAG, TODAY_TAG } from '../tag.const';
-import { createEmptyEntity } from '../../../util/create-empty-entity';
import {
moveTaskDownInTodayList,
moveTaskInTodayList,
moveTaskUpInTodayList,
} from '../../work-context/store/work-context-meta.actions';
import { TaskRepeatCfgService } from '../../task-repeat-cfg/task-repeat-cfg.service';
-import { DateService } from 'src/app/core/date/date.service';
import { PlannerActions } from '../../planner/store/planner.actions';
import { getWorklogStr } from '../../../util/get-work-log-str';
import { deleteProject } from '../../project/store/project.actions';
import { selectTaskById } from '../../tasks/store/task.selectors';
+import { PfapiService } from '../../../pfapi/pfapi.service';
+import { TaskArchiveService } from '../../time-tracking/task-archive.service';
+import { TimeTrackingService } from '../../time-tracking/time-tracking.service';
@Injectable()
export class TagEffects {
private _actions$ = inject(Actions);
private _store$ = inject>(Store);
- private _persistenceService = inject(PersistenceService);
+ private _pfapiService = inject(PfapiService);
private _snackService = inject(SnackService);
private _tagService = inject(TagService);
private _workContextService = inject(WorkContextService);
private _taskService = inject(TaskService);
private _taskRepeatCfgService = inject(TaskRepeatCfgService);
private _router = inject(Router);
- private _dateService = inject(DateService);
+ private _taskArchiveService = inject(TaskArchiveService);
+ private _timeTrackingService = inject(TimeTrackingService);
saveToLs$: Observable = this._store$.pipe(
select(selectTagFeatureState),
take(1),
switchMap((tagState) =>
- this._persistenceService.tag.saveState(tagState, { isSyncModelChange: true }),
+ this._pfapiService.m.tag.save(tagState, { isUpdateRevAndLastUpdate: true }),
),
);
updateTagsStorage$: Observable = createEffect(
@@ -96,9 +90,6 @@ export class TagEffects {
updateTagOrder,
updateAdvancedConfigForTag,
- updateWorkStartForTag,
- updateWorkEndForTag,
- addToBreakTimeForTag,
moveTaskInTagList,
// TASK Actions
@@ -172,57 +163,6 @@ export class TagEffects {
{ dispatch: false },
);
- updateWorkStart$: any = createEffect(() =>
- this._actions$.pipe(
- ofType(addTimeSpent),
- concatMap(({ task }) =>
- task.parentId
- ? this._taskService.getByIdOnce$(task.parentId).pipe(first())
- : of(task),
- ),
- filter((task: Task) => task.tagIds && !!task.tagIds.length),
- concatMap((task: Task) =>
- this._tagService.getTagsByIds$(task.tagIds).pipe(first()),
- ),
- concatMap((tags: Tag[]) =>
- tags
- // only if not assigned for day already
- .filter((tag) => !tag.workStart[this._dateService.todayStr()])
- .map((tag) =>
- updateWorkStartForTag({
- id: tag.id,
- date: this._dateService.todayStr(),
- newVal: Date.now(),
- }),
- ),
- ),
- ),
- );
-
- updateWorkEnd$: Observable = createEffect(() =>
- this._actions$.pipe(
- ofType(addTimeSpent),
- concatMap(({ task }) =>
- task.parentId
- ? this._taskService.getByIdOnce$(task.parentId).pipe(first())
- : of(task),
- ),
- filter((task: Task) => task.tagIds && !!task.tagIds.length),
- concatMap((task: Task) =>
- this._tagService.getTagsByIds$(task.tagIds).pipe(first()),
- ),
- concatMap((tags: Tag[]) =>
- tags.map((tag) =>
- updateWorkEndForTag({
- id: tag.id,
- date: this._dateService.todayStr(),
- newVal: Date.now(),
- }),
- ),
- ),
- ),
- );
-
deleteTagRelatedData: Observable = createEffect(
() =>
this._actions$.pipe(
@@ -232,10 +172,7 @@ export class TagEffects {
// remove from all tasks
this._taskService.removeTagsForAllTask(tagIdsToRemove);
// remove from archive
- await this._persistenceService.taskArchive.execAction(
- removeTagsForAllTasks({ tagIdsToRemove }),
- true,
- );
+ await this._taskArchiveService.removeTagsFromAllTasks(tagIdsToRemove);
const isOrphanedParentTask = (t: Task): boolean =>
!t.projectId && !t.tagIds.length && !t.parentId;
@@ -247,28 +184,10 @@ export class TagEffects {
.map((t) => t.id);
this._taskService.removeMultipleTasks(taskIdsToRemove);
- // remove orphaned for archive
- const taskArchiveState: TaskArchive =
- (await this._persistenceService.taskArchive.loadState()) ||
- createEmptyEntity();
-
- let archiveSubTaskIdsToDelete: string[] = [];
- const archiveMainTaskIdsToDelete: string[] = [];
- (taskArchiveState.ids as string[]).forEach((id) => {
- const t = taskArchiveState.entities[id] as Task;
- if (isOrphanedParentTask(t)) {
- archiveMainTaskIdsToDelete.push(id);
- archiveSubTaskIdsToDelete = archiveSubTaskIdsToDelete.concat(t.subTaskIds);
- }
+ tagIdsToRemove.forEach((id) => {
+ this._timeTrackingService.cleanupDataEverywhereForTag(id);
});
- await this._persistenceService.taskArchive.execAction(
- deleteTasks({
- taskIds: [...archiveMainTaskIdsToDelete, ...archiveSubTaskIdsToDelete],
- }),
- true,
- );
-
// remove from task repeat
const taskRepeatCfgs = await this._taskRepeatCfgService.taskRepeatCfgs$
.pipe(take(1))
diff --git a/src/app/features/tag/store/tag.reducer.ts b/src/app/features/tag/store/tag.reducer.ts
index b8a8fba5f3c..a9edac5c21b 100644
--- a/src/app/features/tag/store/tag.reducer.ts
+++ b/src/app/features/tag/store/tag.reducer.ts
@@ -34,18 +34,14 @@ import { MODEL_VERSION_KEY } from '../../../app.constants';
import { MODEL_VERSION } from '../../../core/model-version';
import {
addTag,
- addToBreakTimeForTag,
deleteTag,
deleteTags,
moveTaskInTagList,
updateAdvancedConfigForTag,
updateTag,
updateTagOrder,
- updateWorkEndForTag,
- updateWorkStartForTag,
upsertTag,
} from './tag.actions';
-import { roundTsToMinutes } from '../../../util/round-ts-to-minutes';
import { PlannerActions } from '../../planner/store/planner.actions';
import { getWorklogStr } from '../../../util/get-work-log-str';
import { moveItemBeforeItem } from '../../../util/move-item-before-item';
@@ -387,57 +383,35 @@ export const tagReducer = createReducer(
};
}),
- on(updateWorkStartForTag, (state: TagState, { id, newVal, date }) =>
- tagAdapter.updateOne(
- {
- id,
- changes: {
- workStart: {
- ...(state.entities[id] as Tag).workStart,
- [date]: roundTsToMinutes(newVal),
- },
- },
- },
- state,
- ),
- ),
-
- on(updateWorkEndForTag, (state: TagState, { id, newVal, date }) =>
- tagAdapter.updateOne(
- {
- id,
- changes: {
- workEnd: {
- ...(state.entities[id] as Tag).workEnd,
- [date]: roundTsToMinutes(newVal),
- },
- },
- },
- state,
- ),
- ),
-
- on(addToBreakTimeForTag, (state: TagState, { id, valToAdd, date }) => {
- const oldTag = state.entities[id] as Tag;
- const oldBreakTime = oldTag.breakTime[date] || 0;
- const oldBreakNr = oldTag.breakNr[date] || 0;
- return tagAdapter.updateOne(
- {
- id,
- changes: {
- breakNr: {
- ...oldTag.breakNr,
- [date]: oldBreakNr + 1,
- },
- breakTime: {
- ...oldTag.breakTime,
- [date]: oldBreakTime + valToAdd,
- },
- },
- },
- state,
- );
- }),
+ // on(updateWorkStartForTag, (state: TagState, { id, newVal, date }) =>
+ // tagAdapter.updateOne(
+ // {
+ // id,
+ // changes: {
+ // workStart: {
+ // ...(state.entities[id] as Tag).workStart,
+ // [date]: roundTsToMinutes(newVal),
+ // },
+ // },
+ // },
+ // state,
+ // ),
+ // ),
+
+ // on(updateWorkEndForTag, (state: TagState, { id, newVal, date }) =>
+ // tagAdapter.updateOne(
+ // {
+ // id,
+ // changes: {
+ // workEnd: {
+ // ...(state.entities[id] as Tag).workEnd,
+ // [date]: roundTsToMinutes(newVal),
+ // },
+ // },
+ // },
+ // state,
+ // ),
+ // ),
on(updateAdvancedConfigForTag, (state: TagState, { tagId, sectionKey, data }) => {
const tagToUpdate = state.entities[tagId] as Tag;
diff --git a/src/app/features/tag/tag-list/tag-list.component.ts b/src/app/features/tag/tag-list/tag-list.component.ts
index 9fd79a2589c..1c4341860ec 100644
--- a/src/app/features/tag/tag-list/tag-list.component.ts
+++ b/src/app/features/tag/tag-list/tag-list.component.ts
@@ -18,6 +18,7 @@ import { selectTagFeatureState } from '../store/tag.reducer';
import { selectProjectFeatureState } from '../../project/store/project.selectors';
import { Project } from '../../project/project.model';
import { TagComponent } from '../tag/tag.component';
+import { DEFAULT_PROJECT_COLOR } from '../../work-context/work-context.const';
@Component({
selector: 'tag-list',
@@ -61,7 +62,7 @@ export class TagListComponent {
if (project) {
const projectTag: Tag = {
...project,
- color: project.theme.primary,
+ color: project.theme.primary || DEFAULT_PROJECT_COLOR,
created: 0,
icon: project.icon || 'folder_special',
};
diff --git a/src/app/features/take-a-break/take-a-break.service.ts b/src/app/features/take-a-break/take-a-break.service.ts
index 758203edcec..76634535788 100644
--- a/src/app/features/take-a-break/take-a-break.service.ts
+++ b/src/app/features/take-a-break/take-a-break.service.ts
@@ -317,7 +317,7 @@ export class TakeABreakService {
cfg.takeABreak.motivationalImgs.length
? cfg.takeABreak.motivationalImgs[
Math.floor(Math.random() * cfg.takeABreak.motivationalImgs.length)
- ]
+ ] || undefined
: undefined,
});
});
diff --git a/src/app/features/task-repeat-cfg/store/task-repeat-cfg.effects.ts b/src/app/features/task-repeat-cfg/store/task-repeat-cfg.effects.ts
index 13028b46562..a02c1a964ed 100644
--- a/src/app/features/task-repeat-cfg/store/task-repeat-cfg.effects.ts
+++ b/src/app/features/task-repeat-cfg/store/task-repeat-cfg.effects.ts
@@ -22,8 +22,7 @@ import {
upsertTaskRepeatCfg,
} from './task-repeat-cfg.actions';
import { selectTaskRepeatCfgFeatureState } from './task-repeat-cfg.reducer';
-import { PersistenceService } from '../../../core/persistence/persistence.service';
-import { Task, TaskArchive, TaskCopy } from '../../tasks/task.model';
+import { Task, TaskCopy } from '../../tasks/task.model';
import { updateTask } from '../../tasks/store/task.actions';
import { TaskService } from '../../tasks/task.service';
import { TaskRepeatCfgService } from '../task-repeat-cfg.service';
@@ -35,7 +34,7 @@ import {
import { forkJoin, from, merge, of } from 'rxjs';
import { setActiveWorkContext } from '../../work-context/store/work-context.actions';
import { SyncTriggerService } from '../../../imex/sync/sync-trigger.service';
-import { SyncProviderService } from '../../../imex/sync/sync-provider.service';
+import { SyncWrapperService } from '../../../imex/sync/sync-wrapper.service';
import { sortRepeatableTaskCfgs } from '../sort-repeatable-task-cfg';
import { MatDialog } from '@angular/material/dialog';
import { DialogConfirmComponent } from '../../../ui/dialog-confirm/dialog-confirm.component';
@@ -45,18 +44,21 @@ import { getDateTimeFromClockString } from '../../../util/get-date-time-from-clo
import { isToday } from '../../../util/is-today.util';
import { DateService } from 'src/app/core/date/date.service';
import { deleteProject } from '../../project/store/project.actions';
+import { PfapiService } from '../../../pfapi/pfapi.service';
+import { TaskArchiveService } from '../../time-tracking/task-archive.service';
@Injectable()
export class TaskRepeatCfgEffects {
private _actions$ = inject(Actions);
private _taskService = inject(TaskService);
private _store$ = inject>(Store);
- private _persistenceService = inject(PersistenceService);
+ private _pfapiService = inject(PfapiService);
private _dateService = inject(DateService);
private _taskRepeatCfgService = inject(TaskRepeatCfgService);
private _syncTriggerService = inject(SyncTriggerService);
- private _syncProviderService = inject(SyncProviderService);
+ private _syncWrapperService = inject(SyncWrapperService);
private _matDialog = inject(MatDialog);
+ private _taskArchiveService = inject(TaskArchiveService);
updateTaskRepeatCfgs$: any = createEffect(
() =>
@@ -82,7 +84,7 @@ export class TaskRepeatCfgEffects {
this._syncTriggerService.afterInitialSyncDoneAndDataLoadedInitially$,
this._actions$.pipe(
ofType(setActiveWorkContext),
- concatMap(() => this._syncProviderService.afterCurrentSyncDoneOrSyncDisabled$),
+ concatMap(() => this._syncWrapperService.afterCurrentSyncDoneOrSyncDisabled$),
),
).pipe(
// make sure everything has settled
@@ -143,7 +145,7 @@ export class TaskRepeatCfgEffects {
this._actions$.pipe(
ofType(deleteTaskRepeatCfg),
tap(({ id }) => {
- this._removeRepeatCfgFromArchiveTasks(id);
+ this._taskArchiveService.removeRepeatCfgFromArchiveTasks(id);
}),
),
{ dispatch: false },
@@ -319,31 +321,8 @@ export class TaskRepeatCfgEffects {
}
private _saveToLs([action, taskRepeatCfgState]: [Action, TaskRepeatCfgState]): void {
- this._persistenceService.taskRepeatCfg.saveState(taskRepeatCfgState, {
- isSyncModelChange: true,
- });
- }
-
- private _removeRepeatCfgFromArchiveTasks(repeatConfigId: string): void {
- this._persistenceService.taskArchive.loadState().then((taskArchive: TaskArchive) => {
- // if not yet initialized for project
- if (!taskArchive) {
- return;
- }
-
- const newState = { ...taskArchive };
- const ids = newState.ids as string[];
-
- const tasksWithRepeatCfgId = ids
- .map((id) => newState.entities[id] as Task)
- .filter((task) => task.repeatCfgId === repeatConfigId);
-
- if (tasksWithRepeatCfgId && tasksWithRepeatCfgId.length) {
- tasksWithRepeatCfgId.forEach((task: any) => (task.repeatCfgId = null));
- this._persistenceService.taskArchive.saveState(newState, {
- isSyncModelChange: true,
- });
- }
+ this._pfapiService.m.taskRepeatCfg.save(taskRepeatCfgState, {
+ isUpdateRevAndLastUpdate: true,
});
}
}
diff --git a/src/app/features/tasks/add-task-bar/add-task-bar.service.ts b/src/app/features/tasks/add-task-bar/add-task-bar.service.ts
index 7a54f6eb135..eded8d17329 100644
--- a/src/app/features/tasks/add-task-bar/add-task-bar.service.ts
+++ b/src/app/features/tasks/add-task-bar/add-task-bar.service.ts
@@ -30,6 +30,7 @@ import { SnackService } from '../../../core/snack/snack.service';
import { T } from '../../../t.const';
import { IssueService } from '../../issue/issue.service';
import { assertTruthy } from '../../../util/assert-truthy';
+import { DEFAULT_PROJECT_COLOR } from '../../work-context/work-context.const';
@Injectable({
providedIn: 'root',
@@ -128,7 +129,7 @@ export class AddTaskBarService {
val,
tags,
projects,
- defaultColor: activeWorkContext.theme.primary,
+ defaultColor: activeWorkContext.theme.primary || DEFAULT_PROJECT_COLOR,
shortSyntaxConfig,
}),
),
diff --git a/src/app/features/tasks/add-task-bar/short-syntax-to-tags.ts b/src/app/features/tasks/add-task-bar/short-syntax-to-tags.ts
index 92b9f591d40..4e1830b6531 100644
--- a/src/app/features/tasks/add-task-bar/short-syntax-to-tags.ts
+++ b/src/app/features/tasks/add-task-bar/short-syntax-to-tags.ts
@@ -1,6 +1,10 @@
import { shortSyntax } from '../short-syntax';
import { msToString } from '../../../ui/duration/ms-to-string.pipe';
-import { DEFAULT_TODAY_TAG_COLOR } from '../../work-context/work-context.const';
+import {
+ DEFAULT_PROJECT_COLOR,
+ DEFAULT_TAG_COLOR,
+ DEFAULT_TODAY_TAG_COLOR,
+} from '../../work-context/work-context.const';
import { Tag } from '../../tag/tag.model';
import { Project } from '../../project/project.model';
import { getWorklogStr } from '../../../util/get-work-log-str';
@@ -50,7 +54,7 @@ export const shortSyntaxToTags = ({
}
shortSyntaxTags.push({
title: project.title,
- color: project.theme.primary,
+ color: project.theme.primary || DEFAULT_PROJECT_COLOR,
projectId: r.projectId,
icon: 'list',
});
@@ -132,7 +136,7 @@ export const shortSyntaxToTags = ({
}
shortSyntaxTags.push({
title: tag.title,
- color: tag.color || tag.theme.primary,
+ color: tag.color || tag.theme.primary || DEFAULT_TAG_COLOR,
icon: tag.icon || 'style',
});
});
diff --git a/src/app/features/tasks/short-syntax.spec.ts b/src/app/features/tasks/short-syntax.spec.ts
index 8023f9c6e73..f6a1e90b927 100644
--- a/src/app/features/tasks/short-syntax.spec.ts
+++ b/src/app/features/tasks/short-syntax.spec.ts
@@ -1,4 +1,4 @@
-import { ShowSubTasksMode, TaskCopy } from './task.model';
+import { TaskCopy } from './task.model';
import { shortSyntax } from './short-syntax';
import { getWorklogStr } from '../../util/get-work-log-str';
import { Tag } from '../tag/tag.model';
@@ -24,8 +24,6 @@ const TASK: TaskCopy = {
repeatCfgId: undefined,
plannedAt: undefined,
- _showSubTasksMode: ShowSubTasksMode.Show,
-
attachments: [],
issueId: undefined,
diff --git a/src/app/features/tasks/store/task-db.effects.ts b/src/app/features/tasks/store/task-db.effects.ts
index aa04fb88fda..f95432282cb 100644
--- a/src/app/features/tasks/store/task-db.effects.ts
+++ b/src/app/features/tasks/store/task-db.effects.ts
@@ -1,10 +1,9 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import {
__updateMultipleTaskSimple,
addSubTask,
addTask,
- addTimeSpent,
convertToMainTask,
deleteTask,
deleteTasks,
@@ -20,8 +19,7 @@ import {
restoreTask,
roundTimeSpentForDay,
scheduleTask,
- toggleStart,
- toggleTaskShowSubTasks,
+ toggleTaskHideSubTasks,
undoDeleteTask,
unScheduleTask,
updateTask,
@@ -29,8 +27,7 @@ import {
updateTaskUi,
} from './task.actions';
import { select, Store } from '@ngrx/store';
-import { tap, withLatestFrom } from 'rxjs/operators';
-import { PersistenceService } from '../../../core/persistence/persistence.service';
+import { auditTime, first, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { selectTaskFeatureState } from './task.selectors';
import { TaskState } from '../task.model';
import { environment } from '../../../../environments/environment';
@@ -42,12 +39,29 @@ import {
} from '../task-attachment/task-attachment.actions';
import { PlannerActions } from '../../planner/store/planner.actions';
import { deleteProject } from '../../project/store/project.actions';
+import { PfapiService } from '../../../pfapi/pfapi.service';
+import { TimeTrackingActions } from '../../time-tracking/store/time-tracking.actions';
+import { TIME_TRACKING_TO_DB_INTERVAL } from '../../../app.constants';
@Injectable()
export class TaskDbEffects {
private _actions$ = inject(Actions);
private _store$ = inject>(Store);
- private _persistenceService = inject(PersistenceService);
+ private _pfapiService = inject(PfapiService);
+
+ updateTaskAuditTime$: any = createEffect(
+ () =>
+ this._actions$.pipe(
+ ofType(
+ // TIME TRACKING
+ TimeTrackingActions.addTimeSpent,
+ ),
+ auditTime(TIME_TRACKING_TO_DB_INTERVAL),
+ switchMap(() => this._store$.pipe(select(selectTaskFeatureState), first())),
+ tap((taskState) => this._saveToLs(taskState, true)),
+ ),
+ { dispatch: false },
+ );
updateTask$: any = createEffect(
() =>
@@ -55,7 +69,6 @@ export class TaskDbEffects {
ofType(
addTask,
restoreTask,
- addTimeSpent,
deleteTask,
deleteTasks,
undoDeleteTask,
@@ -63,6 +76,8 @@ export class TaskDbEffects {
convertToMainTask,
// setCurrentTask,
// unsetCurrentTask,
+ // toggleStart,
+
updateTask,
__updateMultipleTaskSimple,
updateTaskTags,
@@ -74,7 +89,6 @@ export class TaskDbEffects {
moveSubTaskToBottom,
moveToArchive_,
moveToOtherProject,
- toggleStart,
roundTimeSpentForDay,
// REMINDER
@@ -107,7 +121,7 @@ export class TaskDbEffects {
updateTaskUi$: any = createEffect(
() =>
this._actions$.pipe(
- ofType(updateTaskUi, toggleTaskShowSubTasks),
+ ofType(updateTaskUi, toggleTaskHideSubTasks),
withLatestFrom(this._store$.pipe(select(selectTaskFeatureState))),
tap(([, taskState]) => this._saveToLs(taskState)),
),
@@ -115,8 +129,11 @@ export class TaskDbEffects {
);
// @debounce(50)
- private _saveToLs(taskState: TaskState, isSyncModelChange: boolean = false): void {
- this._persistenceService.task.saveState(
+ private _saveToLs(
+ taskState: TaskState,
+ isUpdateRevAndLastUpdate: boolean = false,
+ ): void {
+ this._pfapiService.m.task.save(
{
...taskState,
@@ -124,7 +141,7 @@ export class TaskDbEffects {
selectedTaskId: environment.production ? null : taskState.selectedTaskId,
currentTaskId: null,
},
- { isSyncModelChange },
+ { isUpdateRevAndLastUpdate },
);
}
}
diff --git a/src/app/features/tasks/store/task-electron.effects.ts b/src/app/features/tasks/store/task-electron.effects.ts
index d8c712a2a7b..7266ae6b069 100644
--- a/src/app/features/tasks/store/task-electron.effects.ts
+++ b/src/app/features/tasks/store/task-electron.effects.ts
@@ -1,12 +1,13 @@
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
-import { addTimeSpent, setCurrentTask, unsetCurrentTask } from './task.actions';
+import { setCurrentTask, unsetCurrentTask } from './task.actions';
import { select, Store } from '@ngrx/store';
import { filter, tap, withLatestFrom } from 'rxjs/operators';
import { selectCurrentTask } from './task.selectors';
import { IS_ELECTRON } from '../../../app.constants';
import { GlobalConfigService } from '../../config/global-config.service';
import { selectIsFocusOverlayShown } from '../../focus-mode/store/focus-mode.selectors';
+import { TimeTrackingActions } from '../../time-tracking/store/time-tracking.actions';
// TODO send message to electron when current task changes here
@@ -19,7 +20,7 @@ export class TaskElectronEffects {
taskChangeElectron$: any = createEffect(
() =>
this._actions$.pipe(
- ofType(setCurrentTask, unsetCurrentTask, addTimeSpent),
+ ofType(setCurrentTask, unsetCurrentTask, TimeTrackingActions.addTimeSpent),
withLatestFrom(this._store$.pipe(select(selectCurrentTask))),
tap(([action, current]) => {
if (IS_ELECTRON) {
@@ -53,7 +54,7 @@ export class TaskElectronEffects {
createEffect(
() =>
this._actions$.pipe(
- ofType(addTimeSpent),
+ ofType(TimeTrackingActions.addTimeSpent),
withLatestFrom(
this._configService.cfg$,
this._store$.select(selectIsFocusOverlayShown),
diff --git a/src/app/features/tasks/store/task-internal.effects.ts b/src/app/features/tasks/store/task-internal.effects.ts
index df9032aae0d..c56ef5fc15b 100644
--- a/src/app/features/tasks/store/task-internal.effects.ts
+++ b/src/app/features/tasks/store/task-internal.effects.ts
@@ -73,20 +73,20 @@ export class TaskInternalEffects {
ofType(addTask, addSubTask),
filter(({ task }) => !task.timeEstimate),
withLatestFrom(this._store$.pipe(select(selectConfigFeatureState))),
- filter(
- ([{ task }, cfg]) =>
- (!task.timeEstimate && cfg.timeTracking.defaultEstimate > 0) ||
- cfg.timeTracking.defaultEstimateSubTasks > 0,
- ),
- map(([action, cfg]) =>
+ map(([action, cfg]) => ({
+ timeEstimate:
+ (action.task.parentId || (action.type === addSubTask.type && action.parentId)
+ ? cfg.timeTracking.defaultEstimateSubTasks
+ : cfg.timeTracking.defaultEstimate) || 0,
+ task: action.task,
+ })),
+ filter(({ timeEstimate }) => timeEstimate > 0),
+ map(({ task, timeEstimate }) =>
updateTask({
task: {
- id: action.task.id,
+ id: task.id,
changes: {
- timeEstimate:
- action.task.parentId || (action as any).parentId
- ? cfg.timeTracking.defaultEstimateSubTasks
- : cfg.timeTracking.defaultEstimate,
+ timeEstimate,
},
},
}),
diff --git a/src/app/features/tasks/store/task-related-model.effects.ts b/src/app/features/tasks/store/task-related-model.effects.ts
index 9ebb46a46e9..af4ed6f730a 100644
--- a/src/app/features/tasks/store/task-related-model.effects.ts
+++ b/src/app/features/tasks/store/task-related-model.effects.ts
@@ -1,50 +1,31 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
-import {
- addTimeSpent,
- moveToArchive_,
- restoreTask,
- updateTask,
- updateTaskTags,
-} from './task.actions';
+import { restoreTask, updateTask, updateTaskTags } from './task.actions';
import { concatMap, filter, first, map, switchMap, tap } from 'rxjs/operators';
-import { PersistenceService } from '../../../core/persistence/persistence.service';
-import { Task, TaskArchive, TaskCopy, TaskWithSubTasks } from '../task.model';
-import { ReminderService } from '../../reminder/reminder.service';
+import { Task, TaskCopy } from '../task.model';
import { moveTaskInTodayList } from '../../work-context/store/work-context-meta.actions';
-import { taskAdapter } from './task.adapter';
-import { flattenTasks } from './task.selectors';
import { GlobalConfigService } from '../../config/global-config.service';
import { TODAY_TAG } from '../../tag/tag.const';
import { unique } from '../../../util/unique';
import { TaskService } from '../task.service';
import { EMPTY, Observable, of } from 'rxjs';
-import { createEmptyEntity } from '../../../util/create-empty-entity';
import { moveProjectTaskToRegularList } from '../../project/store/project.actions';
import { SnackService } from '../../../core/snack/snack.service';
import { T } from '../../../t.const';
+import { TimeTrackingActions } from '../../time-tracking/store/time-tracking.actions';
+import { TaskArchiveService } from '../../time-tracking/task-archive.service';
@Injectable()
export class TaskRelatedModelEffects {
private _actions$ = inject(Actions);
- private _reminderService = inject(ReminderService);
private _taskService = inject(TaskService);
private _globalConfigService = inject(GlobalConfigService);
- private _persistenceService = inject(PersistenceService);
private _snackService = inject(SnackService);
+ private _taskArchiveService = inject(TaskArchiveService);
// EFFECTS ===> EXTERNAL
// ---------------------
- moveToArchive$: any = createEffect(
- () =>
- this._actions$.pipe(
- ofType(moveToArchive_),
- tap(({ tasks }) => this._moveToArchive(tasks)),
- ),
- { dispatch: false },
- );
-
restoreTask$: any = createEffect(
() =>
this._actions$.pipe(
@@ -62,7 +43,7 @@ export class TaskRelatedModelEffects {
autoAddTodayTagOnTracking: any = createEffect(() =>
this.ifAutoAddTodayEnabled$(
this._actions$.pipe(
- ofType(addTimeSpent),
+ ofType(TimeTrackingActions.addTimeSpent),
switchMap(({ task }) =>
task.parentId
? this._taskService.getByIdOnce$(task.parentId).pipe(
@@ -221,64 +202,6 @@ export class TaskRelatedModelEffects {
private async _removeFromArchive(task: Task): Promise {
const taskIds = [task.id, ...task.subTaskIds];
- const currentArchive: TaskArchive =
- (await this._persistenceService.taskArchive.loadState()) || createEmptyEntity();
- const allIds = (currentArchive.ids as string[]) || [];
- const idsToRemove: string[] = [];
-
- taskIds.forEach((taskId) => {
- if (allIds.indexOf(taskId) > -1) {
- delete currentArchive.entities[taskId];
- idsToRemove.push(taskId);
- }
- });
-
- return this._persistenceService.taskArchive.saveState(
- {
- ...currentArchive,
- ids: allIds.filter((id) => !idsToRemove.includes(id)),
- },
- { isSyncModelChange: true },
- );
- }
-
- private async _moveToArchive(tasks: TaskWithSubTasks[]): Promise {
- const now = Date.now();
- const flatTasks = flattenTasks(tasks);
- if (!flatTasks.length) {
- return;
- }
-
- const currentArchive: TaskArchive =
- (await this._persistenceService.taskArchive.loadState()) || createEmptyEntity();
-
- const newArchive = taskAdapter.addMany(
- flatTasks.map(({ subTasks, ...task }) => ({
- ...task,
- reminderId: undefined,
- isDone: true,
- plannedAt: undefined,
- doneOn:
- task.isDone && task.doneOn
- ? task.doneOn
- : task.parentId
- ? flatTasks.find((t) => t.id === task.parentId)?.doneOn || now
- : now,
- })),
- currentArchive,
- );
-
- flatTasks
- .filter((t) => !!t.reminderId)
- .forEach((t) => {
- if (!t.reminderId) {
- throw new Error('No t.reminderId');
- }
- this._reminderService.removeReminder(t.reminderId);
- });
-
- return this._persistenceService.taskArchive.saveState(newArchive, {
- isSyncModelChange: true,
- });
+ return this._taskArchiveService.deleteTasks(taskIds);
}
}
diff --git a/src/app/features/tasks/store/task-reminder.effects.ts b/src/app/features/tasks/store/task-reminder.effects.ts
index dbac7ffa81d..b41994f8c0b 100644
--- a/src/app/features/tasks/store/task-reminder.effects.ts
+++ b/src/app/features/tasks/store/task-reminder.effects.ts
@@ -3,6 +3,7 @@ import { Actions, createEffect, ofType } from '@ngrx/effects';
import {
deleteTask,
deleteTasks,
+ moveToArchive_,
reScheduleTask,
scheduleTask,
unScheduleTask,
@@ -22,6 +23,7 @@ import { DEFAULT_DAY_START } from '../../config/default-global-config.const';
import { moveProjectTaskToBacklogListAuto } from '../../project/store/project.actions';
import { isSameDay } from '../../../util/is-same-day';
import { isToday } from '../../../util/is-today.util';
+import { flattenTasks } from './task.selectors';
@Injectable()
export class TaskReminderEffects {
@@ -182,6 +184,28 @@ export class TaskReminderEffects {
),
{ dispatch: false },
);
+
+ clearRemindersForArchivedTasks: any = createEffect(
+ () =>
+ this._actions$.pipe(
+ ofType(moveToArchive_),
+ tap(({ tasks }) => {
+ const flatTasks = flattenTasks(tasks);
+ if (!flatTasks.length) {
+ return;
+ }
+ flatTasks
+ .filter((t) => !!t.reminderId)
+ .forEach((t) => {
+ if (!t.reminderId) {
+ throw new Error('No t.reminderId');
+ }
+ this._reminderService.removeReminder(t.reminderId);
+ });
+ }),
+ ),
+ { dispatch: false },
+ );
clearMultipleReminders: any = createEffect(
() =>
this._actions$.pipe(
diff --git a/src/app/features/tasks/store/task.actions.ts b/src/app/features/tasks/store/task.actions.ts
index b2a1749bbdb..ba421589bf3 100644
--- a/src/app/features/tasks/store/task.actions.ts
+++ b/src/app/features/tasks/store/task.actions.ts
@@ -15,7 +15,7 @@ enum TaskActionTypes {
'UpdateTaskUi' = '[Task] Update Task Ui',
'UpdateTaskTags' = '[Task] Update Task Tags',
'RemoveTagsForAllTasks' = '[Task] Remove Tags from all Tasks',
- 'ToggleTaskShowSubTasks' = '[Task] Toggle Show Sub Tasks',
+ 'ToggleTaskHideSubTasks' = '[Task] Toggle Show Sub Tasks',
'UpdateTask' = '[Task] Update Task',
'UpdateMTasksSimple' = '[Task] Update multiple Tasks (simple)',
'UpdateTasks' = '[Task] Update Tasks',
@@ -115,8 +115,8 @@ export const removeTagsForAllTasks = createAction(
props<{ tagIdsToRemove: string[] }>(),
);
-export const toggleTaskShowSubTasks = createAction(
- TaskActionTypes.ToggleTaskShowSubTasks,
+export const toggleTaskHideSubTasks = createAction(
+ TaskActionTypes.ToggleTaskHideSubTasks,
props<{ taskId: string; isShowLess: boolean; isEndless: boolean }>(),
);
@@ -172,17 +172,6 @@ export const moveSubTaskToBottom = createAction(
props<{ id: string; parentId: string }>(),
);
-export const addTimeSpent = createAction(
- TaskActionTypes.AddTimeSpent,
-
- props<{
- task: Task;
- date: string;
- duration: number;
- isFromTrackingReminder: boolean;
- }>(),
-);
-
export const removeTimeSpent = createAction(
TaskActionTypes.RemoveTimeSpent,
diff --git a/src/app/features/tasks/store/task.reducer.ts b/src/app/features/tasks/store/task.reducer.ts
index 6ff370b1ae0..1a04f90c499 100644
--- a/src/app/features/tasks/store/task.reducer.ts
+++ b/src/app/features/tasks/store/task.reducer.ts
@@ -2,7 +2,6 @@ import {
__updateMultipleTaskSimple,
addSubTask,
addTask,
- addTimeSpent,
convertToMainTask,
deleteTask,
deleteTasks,
@@ -22,14 +21,14 @@ import {
setCurrentTask,
setSelectedTask,
toggleStart,
- toggleTaskShowSubTasks,
+ toggleTaskHideSubTasks,
unScheduleTask,
unsetCurrentTask,
updateTask,
updateTaskTags,
updateTaskUi,
} from './task.actions';
-import { ShowSubTasksMode, Task, TaskDetailTargetPanel, TaskState } from '../task.model';
+import { Task, TaskDetailTargetPanel, TaskState } from '../task.model';
import { calcTotalTimeSpent } from '../util/calc-total-time-spent';
import { addTaskRepeatCfgToTask } from '../../task-repeat-cfg/store/task-repeat-cfg.actions';
import {
@@ -68,6 +67,7 @@ import { PlannerActions } from '../../planner/store/planner.actions';
import { TODAY_TAG } from '../../tag/tag.const';
import { getWorklogStr } from '../../../util/get-work-log-str';
import { deleteProject } from '../../project/store/project.actions';
+import { TimeTrackingActions } from '../../time-tracking/store/time-tracking.actions';
export const TASK_FEATURE_NAME = 'tasks';
@@ -113,6 +113,21 @@ export const taskReducer = createReducer(
});
}),
+ on(TimeTrackingActions.addTimeSpent, (state, { task, date, duration }) => {
+ const currentTimeSpentForTickDay =
+ (task.timeSpentOnDay && +task.timeSpentOnDay[date]) || 0;
+ return updateTimeSpentForTask(
+ task.id,
+ {
+ ...task.timeSpentOnDay,
+ [date]: currentTimeSpentForTickDay + duration,
+ },
+ state,
+ );
+ }),
+
+ //--------------------------------
+
// TODO check if working
on(setCurrentTask, (state, { id }) => {
if (id) {
@@ -231,51 +246,42 @@ export const taskReducer = createReducer(
}),
// TODO simplify
- on(toggleTaskShowSubTasks, (state, { taskId, isShowLess, isEndless }) => {
+ on(toggleTaskHideSubTasks, (state, { taskId, isShowLess, isEndless }) => {
const task = getTaskById(taskId, state);
const subTasks = task.subTaskIds.map((id) => getTaskById(id, state));
const doneTasksLength = subTasks.filter((t) => t.isDone).length;
const isDoneTaskCaseNeeded = doneTasksLength && doneTasksLength < subTasks.length;
- const oldVal = +task._showSubTasksMode;
- let newVal;
-
- if (isDoneTaskCaseNeeded) {
- newVal = oldVal + (isShowLess ? -1 : 1);
- if (isEndless) {
- if (newVal > ShowSubTasksMode.Show) {
- newVal = ShowSubTasksMode.HideAll;
- } else if (newVal < ShowSubTasksMode.HideAll) {
- newVal = ShowSubTasksMode.Show;
- }
+ // for easier calculations we use 0 instead of undefined for show state
+ const oldVal = task._hideSubTasksMode || 0;
+ let newVal: number = isShowLess ? oldVal + 1 : oldVal - 1;
+
+ if (!isDoneTaskCaseNeeded && newVal === 1) {
+ if (isShowLess) {
+ newVal = 2;
} else {
- if (newVal > ShowSubTasksMode.Show) {
- newVal = ShowSubTasksMode.Show;
- }
- if (newVal < ShowSubTasksMode.HideAll) {
- newVal = ShowSubTasksMode.HideAll;
- }
+ newVal = 0;
+ }
+ }
+
+ if (isEndless) {
+ if (newVal < 0) {
+ newVal = 2;
+ } else if (newVal > 2) {
+ newVal = 0;
}
} else {
- if (isEndless) {
- if (oldVal === ShowSubTasksMode.Show) {
- newVal = ShowSubTasksMode.HideAll;
- }
- if (oldVal !== ShowSubTasksMode.Show) {
- newVal = ShowSubTasksMode.Show;
- }
- } else {
- newVal = isShowLess ? ShowSubTasksMode.HideAll : ShowSubTasksMode.Show;
+ if (newVal < 0) {
+ newVal = 0;
+ } else if (newVal > 2) {
+ newVal = 2;
}
}
- // failsafe
- newVal = isNaN(newVal as any) ? ShowSubTasksMode.HideAll : newVal;
-
return taskAdapter.updateOne(
{
id: taskId,
changes: {
- _showSubTasksMode: newVal,
+ _hideSubTasksMode: newVal || undefined,
},
},
state,
@@ -395,19 +401,6 @@ export const taskReducer = createReducer(
);
}),
- on(addTimeSpent, (state, { task, date, duration }) => {
- const currentTimeSpentForTickDay =
- (task.timeSpentOnDay && +task.timeSpentOnDay[date]) || 0;
- return updateTimeSpentForTask(
- task.id,
- {
- ...task.timeSpentOnDay,
- [date]: currentTimeSpentForTickDay + duration,
- },
- state,
- );
- }),
-
on(removeTimeSpent, (state, { id, date, duration }) => {
const task = getTaskById(id, state);
const currentTimeSpentForTickDay =
diff --git a/src/app/features/tasks/task-attachment/task-attachment-list/task-attachment-list.component.ts b/src/app/features/tasks/task-attachment/task-attachment-list/task-attachment-list.component.ts
index 8ade79f1eff..5531de28d42 100644
--- a/src/app/features/tasks/task-attachment/task-attachment-list/task-attachment-list.component.ts
+++ b/src/app/features/tasks/task-attachment/task-attachment-list/task-attachment-list.component.ts
@@ -74,7 +74,7 @@ export class TaskAttachmentListComponent {
// Force-cast PermissionDescriptor as 'clipboard-write' is not defined in type
const permission = { name: 'clipboard-write' } as unknown as PermissionDescriptor;
const result = await navigator.permissions.query(permission);
- if (result.state == 'granted' || result.state == 'prompt') {
+ if ((result.state == 'granted' || result.state == 'prompt') && attachment.path) {
await navigator.clipboard.writeText(attachment.path);
this._snackService.open(T.GLOBAL_SNACK.COPY_TO_CLIPPBOARD);
}
diff --git a/src/app/features/tasks/task-attachment/task-attachment.model.ts b/src/app/features/tasks/task-attachment/task-attachment.model.ts
index ba5952eaefb..cfdad622598 100644
--- a/src/app/features/tasks/task-attachment/task-attachment.model.ts
+++ b/src/app/features/tasks/task-attachment/task-attachment.model.ts
@@ -5,9 +5,19 @@ import {
export type TaskAttachmentType = DropPasteInputType;
-export interface TaskAttachmentCopy extends DropPasteInput {
+// export interface TaskAttachmentCopy extends DropPasteInput {
+// id: string | null;
+// originalImgPath?: string;
+// }
+
+export interface TaskAttachmentCopy extends Partial {
id: string | null;
originalImgPath?: string;
+ // made optional to make them compatible with legacy type (see above)
+ // TODO properly migrate instead maybe
+ type: DropPasteInputType;
+ title?: string;
+ icon?: string;
}
export type TaskAttachment = Readonly;
diff --git a/src/app/features/tasks/task-detail-panel/task-detail-panel.component.html b/src/app/features/tasks/task-detail-panel/task-detail-panel.component.html
index a0b911d8cc8..61d60149dc0 100644
--- a/src/app/features/tasks/task-detail-panel/task-detail-panel.component.html
+++ b/src/app/features/tasks/task-detail-panel/task-detail-panel.component.html
@@ -31,8 +31,8 @@
@if (task.subTasks?.length) {
();
- ShowSubTasksMode: typeof ShowSubTasksMode = ShowSubTasksMode;
+ ShowSubTasksMode: typeof HideSubTasksMode = HideSubTasksMode;
selectedItemIndex: number = 0;
isFocusNotes: boolean = false;
isDragOver: boolean = false;
diff --git a/src/app/features/tasks/task.model.ts b/src/app/features/tasks/task.model.ts
index 9c0024dc277..34b8900ec31 100644
--- a/src/app/features/tasks/task.model.ts
+++ b/src/app/features/tasks/task.model.ts
@@ -4,10 +4,10 @@ import { EntityState } from '@ngrx/entity';
import { TaskAttachment } from './task-attachment/task-attachment.model';
import { MODEL_VERSION_KEY } from '../../app.constants';
-export enum ShowSubTasksMode {
- HideAll = 0,
+export enum HideSubTasksMode {
+ // Show is undefined
HideDone = 1,
- Show = 2,
+ HideAll = 2,
}
export enum TaskDetailTargetPanel {
@@ -69,9 +69,12 @@ export interface TaskCopy extends IssueFieldsForTask {
title: string;
subTaskIds: string[];
+
timeSpentOnDay: TimeSpentOnDay;
- timeSpent: number;
timeEstimate: number;
+ timeSpent: number;
+
+ notes?: string;
created: number;
isDone: boolean;
@@ -80,8 +83,6 @@ export interface TaskCopy extends IssueFieldsForTask {
hasPlannedTime?: boolean;
// remindCfg: TaskReminderOptionId;
- notes: string;
-
parentId?: string;
reminderId?: string;
repeatCfgId?: string;
@@ -92,8 +93,8 @@ export interface TaskCopy extends IssueFieldsForTask {
attachments: TaskAttachment[];
// ui model
- // 0: show, 1: hide-done tasks, 2: hide all sub tasks
- _showSubTasksMode: ShowSubTasksMode;
+ // 0: show, 1: hide-done tasks, 2: hide all sub-tasks
+ _hideSubTasksMode?: HideSubTasksMode;
}
/**
@@ -145,12 +146,9 @@ export const DEFAULT_TASK: Task = {
timeEstimate: 0,
isDone: false,
title: '',
- notes: '',
tagIds: [],
created: Date.now(),
- _showSubTasksMode: ShowSubTasksMode.Show,
-
attachments: [],
};
@@ -161,7 +159,7 @@ export interface TaskState extends EntityState {
// additional entities state properties
currentTaskId: string | null;
selectedTaskId: string | null;
- taskDetailTargetPanel: TaskDetailTargetPanel | null;
+ taskDetailTargetPanel?: TaskDetailTargetPanel | null;
lastCurrentTaskId: string | null;
isDataLoaded: boolean;
diff --git a/src/app/features/tasks/task.service.ts b/src/app/features/tasks/task.service.ts
index 512160bb4db..8d2150db60e 100644
--- a/src/app/features/tasks/task.service.ts
+++ b/src/app/features/tasks/task.service.ts
@@ -1,12 +1,12 @@
import { nanoid } from 'nanoid';
import { first, map, take, withLatestFrom } from 'rxjs/operators';
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import {
ArchiveTask,
DEFAULT_TASK,
DropListModelSource,
- ShowSubTasksMode,
+ HideSubTasksMode,
Task,
TaskArchive,
TaskCopy,
@@ -19,7 +19,6 @@ import { select, Store } from '@ngrx/store';
import {
addSubTask,
addTask,
- addTimeSpent,
convertToMainTask,
deleteTask,
deleteTasks,
@@ -39,14 +38,13 @@ import {
setCurrentTask,
setSelectedTask,
toggleStart,
- toggleTaskShowSubTasks,
+ toggleTaskHideSubTasks,
unScheduleTask,
unsetCurrentTask,
updateTask,
updateTaskTags,
updateTaskUi,
} from './store/task.actions';
-import { PersistenceService } from '../../core/persistence/persistence.service';
import { IssueProviderKey } from '../issue/issue.model';
import { GlobalTrackingIntervalService } from '../../core/global-tracking-interval/global-tracking-interval.service';
import {
@@ -83,7 +81,7 @@ import {
} from '../work-context/store/work-context-meta.actions';
import { Router } from '@angular/router';
import { unique } from '../../util/unique';
-import { ImexMetaService } from '../../imex/imex-meta/imex-meta.service';
+import { ImexViewService } from '../../imex/imex-meta/imex-view.service';
import { remindOptionToMilliseconds } from './util/remind-option-to-milliseconds';
import {
moveProjectTaskDownInBacklogList,
@@ -96,19 +94,23 @@ import {
} from '../project/store/project.actions';
import { Update } from '@ngrx/entity';
import { DateService } from 'src/app/core/date/date.service';
+import { TimeTrackingActions } from '../time-tracking/store/time-tracking.actions';
+import { ArchiveService } from '../time-tracking/archive.service';
+import { TaskArchiveService } from '../time-tracking/task-archive.service';
@Injectable({
providedIn: 'root',
})
export class TaskService {
private readonly _store = inject>(Store);
- private readonly _persistenceService = inject(PersistenceService);
private readonly _tagService = inject(TagService);
private readonly _workContextService = inject(WorkContextService);
- private readonly _imexMetaService = inject(ImexMetaService);
+ private readonly _imexMetaService = inject(ImexViewService);
private readonly _timeTrackingService = inject(GlobalTrackingIntervalService);
private readonly _dateService = inject(DateService);
private readonly _router = inject(Router);
+ private readonly _archiveService = inject(ArchiveService);
+ private readonly _taskArchiveService = inject(TaskArchiveService);
// Currently used in idle service TODO remove
currentTaskId: string | null = null;
@@ -142,7 +144,7 @@ export class TaskService {
map((tasks) => tasks[0]),
);
- taskDetailPanelTargetPanel$: Observable =
+ taskDetailPanelTargetPanel$: Observable =
this._store.pipe(
select(selectTaskDetailTargetPanel),
// NOTE: we can't use share here, as we need the last emitted value
@@ -631,7 +633,9 @@ export class TaskService {
date: string = this._dateService.todayStr(),
isFromTrackingReminder = false,
): void {
- this._store.dispatch(addTimeSpent({ task, date, duration, isFromTrackingReminder }));
+ this._store.dispatch(
+ TimeTrackingActions.addTimeSpent({ task, date, duration, isFromTrackingReminder }),
+ );
}
removeTimeSpent(
@@ -692,6 +696,7 @@ export class TaskService {
});
}
this._store.dispatch(moveToArchive_({ tasks: tasks.filter((t) => !t.parentId) }));
+ this._archiveService.moveTasksToArchiveAndFlushArchiveIfDue(tasks);
}
moveToProject(task: TaskWithSubTasks, projectId: string): void {
@@ -749,10 +754,13 @@ export class TaskService {
);
// archive
- await this._persistenceService.taskArchive.execAction(
- roundTimeSpentForDay({ day, taskIds: archivedIds, roundTo, isRoundUp, projectId }),
- true,
- );
+ await this._taskArchiveService.roundTimeSpent({
+ day,
+ taskIds: archivedIds,
+ roundTo,
+ isRoundUp,
+ projectId,
+ });
}
// REMINDER
@@ -854,7 +862,7 @@ export class TaskService {
}
showSubTasks(id: string): void {
- this.updateUi(id, { _showSubTasksMode: ShowSubTasksMode.Show });
+ this.updateUi(id, { _hideSubTasksMode: undefined });
}
toggleSubTaskMode(
@@ -862,11 +870,11 @@ export class TaskService {
isShowLess: boolean = true,
isEndless: boolean = false,
): void {
- this._store.dispatch(toggleTaskShowSubTasks({ taskId, isShowLess, isEndless }));
+ this._store.dispatch(toggleTaskHideSubTasks({ taskId, isShowLess, isEndless }));
}
hideSubTasks(id: string): void {
- this.updateUi(id, { _showSubTasksMode: ShowSubTasksMode.HideAll });
+ this.updateUi(id, { _hideSubTasksMode: HideSubTasksMode.HideAll });
}
async convertToMainTask(task: Task): Promise {
@@ -888,46 +896,32 @@ export class TaskService {
}
}
+ // TODO remove in favor of calling this directly
// BEWARE: does only work for task model updates, but not the meta models
async updateArchiveTask(id: string, changedFields: Partial): Promise {
- await this._persistenceService.taskArchive.execAction(
- updateTask({
- task: {
- id,
- changes: changedFields,
- },
- }),
- true,
- );
+ return this._taskArchiveService.updateTask(id, changedFields);
}
// BEWARE: does only work for task model updates, but not the meta models
async updateArchiveTasks(updates: Update[]): Promise {
- await this._persistenceService.taskArchive.execActions(
- updates.map((upd) => updateTask({ task: upd })),
- true,
- );
+ return this._taskArchiveService.updateTasks(updates);
}
async getByIdFromEverywhere(id: string, isArchive?: boolean): Promise {
if (isArchive === undefined) {
- return (
- (await this._persistenceService.task.getById(id)) ||
- (await this._persistenceService.taskArchive.getById(id))
- );
+ return this.getByIdOnce$(id).toPromise() || this._taskArchiveService.getById(id);
}
if (isArchive) {
- return await this._persistenceService.taskArchive.getById(id);
+ return await this._taskArchiveService.getById(id);
} else {
- return await this._persistenceService.task.getById(id);
+ return await this.getByIdOnce$(id).toPromise();
}
}
async getAllTasksForProject(projectId: string): Promise {
const allTasks = await this._allTasks$.pipe(first()).toPromise();
- const archiveTaskState: TaskArchive =
- await this._persistenceService.taskArchive.loadState();
+ const archiveTaskState: TaskArchive = await this._taskArchiveService.load();
const ids = (archiveTaskState && (archiveTaskState.ids as string[])) || [];
const archiveTasks = ids.map((id) => archiveTaskState.entities[id]);
return [...allTasks, ...archiveTasks].filter(
@@ -936,8 +930,7 @@ export class TaskService {
}
async getArchiveTasksForRepeatCfgId(repeatCfgId: string): Promise {
- const archiveTaskState: TaskArchive =
- await this._persistenceService.taskArchive.loadState();
+ const archiveTaskState: TaskArchive = await this._taskArchiveService.load();
const ids = (archiveTaskState && (archiveTaskState.ids as string[])) || [];
const archiveTasks = ids.map((id) => archiveTaskState.entities[id]);
return archiveTasks.filter(
@@ -946,8 +939,7 @@ export class TaskService {
}
async getArchivedTasks(): Promise {
- const archiveTaskState: TaskArchive =
- await this._persistenceService.taskArchive.loadState(true);
+ const archiveTaskState: TaskArchive = await this._taskArchiveService.load(true);
const ids = (archiveTaskState && (archiveTaskState.ids as string[])) || [];
const archiveTasks = ids.map((id) => archiveTaskState.entities[id]) as Task[];
return archiveTasks;
@@ -972,8 +964,7 @@ export class TaskService {
async getAllTasksEverywhere(): Promise {
const allTasks = await this._allTasks$.pipe(first()).toPromise();
- const archiveTaskState: TaskArchive =
- await this._persistenceService.taskArchive.loadState();
+ const archiveTaskState: TaskArchive = await this._taskArchiveService.load();
const ids = (archiveTaskState && (archiveTaskState.ids as string[])) || [];
const archiveTasks = ids.map((id) => archiveTaskState.entities[id]);
return [...allTasks, ...archiveTasks] as Task[];
@@ -1009,8 +1000,7 @@ export class TaskService {
subTasks: null,
};
} else {
- const archiveTaskState: TaskArchive =
- await this._persistenceService.taskArchive.loadState();
+ const archiveTaskState: TaskArchive = await this._taskArchiveService.load();
const ids = archiveTaskState && (archiveTaskState.ids as string[]);
if (ids) {
const archiveTaskWithSameIssue = ids
diff --git a/src/app/features/tasks/task/task.component.html b/src/app/features/tasks/task/task.component.html
index 17a21e6b33c..2766033828f 100644
--- a/src/app/features/tasks/task/task.component.html
+++ b/src/app/features/tasks/task/task.component.html
@@ -291,12 +291,12 @@
color=""
mat-mini-fab
>
- @if (t._showSubTasksMode === ShowSubTasksMode.HideAll) {
+ @if (t._hideSubTasksMode === ShowSubTasksMode.HideAll) {
add
}
- @if (t._showSubTasksMode !== ShowSubTasksMode.HideAll) {
+ @if (t._hideSubTasksMode !== ShowSubTasksMode.HideAll) {
remove
}
@@ -305,8 +305,8 @@
@if (t.subTasks?.length) {
('taskTitleEditEl');
@@ -911,7 +911,7 @@ export class TaskComponent implements OnDestroy, AfterViewInit {
const hasSubTasks = t.subTasks && (t.subTasks as any).length > 0;
if (this.isSelected()) {
this.hideDetailPanel();
- } else if (hasSubTasks && t._showSubTasksMode !== ShowSubTasksMode.HideAll) {
+ } else if (hasSubTasks && t._hideSubTasksMode !== HideSubTasksMode.HideAll) {
this._taskService.toggleSubTaskMode(t.id, true, false);
// TODO find a solution
// } else if (this.task.parentId) {
@@ -924,7 +924,7 @@ export class TaskComponent implements OnDestroy, AfterViewInit {
// expand sub tasks
if (ev.key === 'ArrowRight' || checkKeyCombo(ev, keys.expandSubTasks)) {
const hasSubTasks = t.subTasks && (t.subTasks as any).length > 0;
- if (hasSubTasks && t._showSubTasksMode !== ShowSubTasksMode.Show) {
+ if (hasSubTasks && t._hideSubTasksMode !== undefined) {
this._taskService.toggleSubTaskMode(t.id, false, false);
} else if (!this.isSelected()) {
this.showDetailPanel();
diff --git a/src/app/imex/sync/NEW-tracking.model.ts b/src/app/features/time-tracking/NEW-tracking.model.ts
similarity index 100%
rename from src/app/imex/sync/NEW-tracking.model.ts
rename to src/app/features/time-tracking/NEW-tracking.model.ts
diff --git a/src/app/features/time-tracking/archive.service.spec.ts b/src/app/features/time-tracking/archive.service.spec.ts
new file mode 100644
index 00000000000..ab1cba264b3
--- /dev/null
+++ b/src/app/features/time-tracking/archive.service.spec.ts
@@ -0,0 +1,16 @@
+// import { TestBed } from '@angular/core/testing';
+//
+// import { ArchiveService } from './archive.service';
+//
+// describe('ArchiveService', () => {
+// let service: ArchiveService;
+//
+// beforeEach(() => {
+// TestBed.configureTestingModule({});
+// service = TestBed.inject(ArchiveService);
+// });
+//
+// it('should be created', () => {
+// expect(service).toBeTruthy();
+// });
+// });
diff --git a/src/app/features/time-tracking/archive.service.ts b/src/app/features/time-tracking/archive.service.ts
new file mode 100644
index 00000000000..51536a9b156
--- /dev/null
+++ b/src/app/features/time-tracking/archive.service.ts
@@ -0,0 +1,140 @@
+import { inject, Injectable } from '@angular/core';
+import { TaskWithSubTasks } from '../tasks/task.model';
+import { flattenTasks } from '../tasks/store/task.selectors';
+import { createEmptyEntity } from '../../util/create-empty-entity';
+import { taskAdapter } from '../tasks/store/task.adapter';
+import { PfapiService } from '../../pfapi/pfapi.service';
+import {
+ sortTimeTrackingAndTasksFromArchiveYoungToOld,
+ sortTimeTrackingDataToArchiveYoung,
+} from './sort-data-to-flush';
+import { Store } from '@ngrx/store';
+import { TimeTrackingActions } from './store/time-tracking.actions';
+import { getWorklogStr } from '../../util/get-work-log-str';
+
+/*
+# Considerations for flush architecture:
+** The main purpose of flushing is mainly to reduce the amount of data that needs to be transferred over the network **
+Roughly we aim at these 3 syncs to occur under normal circumstances:
+
+every sync => sync the meta file
+daily => +archiveYoung (moving tasks to archive)
+less often => +archiveOld (after flushing data from archiveYoung to archiveOld)
+
+## Other considerations:
+
+timeTracking:
+* (currently) there seems to be no writing of archiveYoung or archiveOld like there is for archive tasks, when editing tasks in worklog
+=> archiveOld.timeTracking and archiveYoung.timeTracking can be read-only
+* data for today should never be in the archive and always be in the store to avoid problems when doing partial updates
+=> timeTracking should always retain some data, at least for today (or maybe later for the whole current week, if we want to make it editable)
+
+taskArchive:
+* data in archiveYoung should be faster to access and write
+* when updating some old data, we need to upload archiveOld regardless of flushing
+=> makes sense to retain data in archiveYoung that is likely to be accessed more often
+=> 21 days is maybe a good middle ground for this, since it allows us to write data from the last month
+ */
+
+export const ARCHIVE_ALL_YOUNG_TO_OLD_THRESHOLD = 1000 * 60 * 60 * 24 * 14;
+export const ARCHIVE_TASK_YOUNG_TO_OLD_THRESHOLD = 1000 * 60 * 60 * 24 * 21;
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ArchiveService {
+ private readonly _pfapiService = inject(PfapiService);
+ private readonly _store = inject(Store);
+
+ // NOTE: we choose this method as trigger to check for flushing to archive, since
+ // it is usually triggered every work-day once
+ async moveTasksToArchiveAndFlushArchiveIfDue(tasks: TaskWithSubTasks[]): Promise {
+ const now = Date.now();
+ const flatTasks = flattenTasks(tasks);
+ if (!flatTasks.length) {
+ return;
+ }
+
+ const archiveYoung = await this._pfapiService.m.archiveYoung.load();
+ const taskArchiveState = archiveYoung.task || createEmptyEntity();
+
+ const newTaskArchive = taskAdapter.addMany(
+ flatTasks.map(({ subTasks, ...task }) => ({
+ ...task,
+ reminderId: undefined,
+ isDone: true,
+ plannedAt: undefined,
+ _hideSubTasksMode: undefined,
+ doneOn:
+ task.isDone && task.doneOn
+ ? task.doneOn
+ : task.parentId
+ ? flatTasks.find((t) => t.id === task.parentId)?.doneOn || now
+ : now,
+ })),
+ taskArchiveState,
+ );
+
+ // ------------------------------------------------
+ // Result A:
+ // Move all archived tasks to archiveYoung
+ // Move timeTracking data to archiveYoung
+ const newSorted1 = sortTimeTrackingDataToArchiveYoung({
+ // TODO think if it is better to get this from store as it is fresher potentially
+ timeTracking: await this._pfapiService.m.timeTracking.load(),
+ archiveYoung,
+ todayStr: getWorklogStr(now),
+ });
+ const newArchiveYoung = {
+ ...newSorted1.archiveYoung,
+ task: newTaskArchive,
+ lastFlush: now,
+ };
+ await this._pfapiService.m.archiveYoung.save(newArchiveYoung, {
+ isUpdateRevAndLastUpdate: true,
+ });
+ this._store.dispatch(
+ TimeTrackingActions.updateWholeState({
+ newState: newSorted1.timeTracking,
+ }),
+ );
+
+ // ------------------------------------------------
+ const archiveOld = await this._pfapiService.m.archiveOld.load();
+ const isFlushArchiveOld =
+ now - archiveOld.lastTimeTrackingFlush > ARCHIVE_ALL_YOUNG_TO_OLD_THRESHOLD;
+
+ if (!isFlushArchiveOld) {
+ return;
+ }
+
+ // ------------------------------------------------
+ // Result B:
+ // Also sort timeTracking and task data from archiveYoung to archiveOld
+ const newSorted2 = sortTimeTrackingAndTasksFromArchiveYoungToOld({
+ archiveYoung: newArchiveYoung,
+ archiveOld,
+ threshold: ARCHIVE_TASK_YOUNG_TO_OLD_THRESHOLD,
+ now,
+ });
+ await this._pfapiService.m.archiveYoung.save(
+ {
+ ...newSorted2.archiveYoung,
+ lastTimeTrackingFlush: now,
+ },
+ {
+ isUpdateRevAndLastUpdate: true,
+ },
+ );
+ await this._pfapiService.m.archiveOld.save(
+ {
+ ...newSorted2.archiveOld,
+ lastTimeTrackingFlush: now,
+ },
+ {
+ isUpdateRevAndLastUpdate: true,
+ },
+ );
+ alert('FLUSHED ALL FROM ARCHIVE YOUNG TO OLD');
+ }
+}
diff --git a/src/app/features/time-tracking/merge-time-tracking-states.spec.ts b/src/app/features/time-tracking/merge-time-tracking-states.spec.ts
new file mode 100644
index 00000000000..a9d27598b93
--- /dev/null
+++ b/src/app/features/time-tracking/merge-time-tracking-states.spec.ts
@@ -0,0 +1,124 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import {
+ mergeTimeTrackingStates,
+ mergeTimeTrackingStatesForWorkContext,
+} from './merge-time-tracking-states';
+import { TimeTrackingState, TTWorkContextSessionMap } from './time-tracking.model';
+
+describe('mergeTimeTrackingStates', () => {
+ it('should merge time tracking states so newest one wins', () => {
+ const current: TimeTrackingState = {
+ project: { '1': { '2023-01-01': { s: 1, e: 2, b: 3, bt: 4 } } },
+ tag: { '2': { '2023-01-02': { s: 5, e: 6, b: 7, bt: 8 } } },
+ };
+ const archiveYoung: TimeTrackingState = {
+ project: { '1': { '2023-01-01': { s: 9, e: 10, b: 11, bt: 12 } } },
+ tag: { '2': { '2023-01-02': { s: 13, e: 14, b: 15, bt: 16 } } },
+ };
+ const archiveOld: TimeTrackingState = {
+ project: { '1': { '2023-01-01': { s: 17, e: 18, b: 19, bt: 20 } } },
+ tag: { '2': { '2023-01-02': { s: 21, e: 22, b: 23, bt: 24 } } },
+ };
+
+ const result = mergeTimeTrackingStates({
+ current,
+ archiveYoung: archiveYoung,
+ archiveOld: archiveOld,
+ });
+
+ expect(result).toEqual({
+ project: { 1: { '2023-01-01': { b: 3, bt: 4, e: 2, s: 1 } } },
+ tag: { 2: { '2023-01-02': { b: 7, bt: 8, e: 6, s: 5 } } },
+ });
+ });
+});
+
+describe('mergeTimeTrackingStatesForWorkContext', () => {
+ it('should merge time tracking states for work context correctly', () => {
+ const current: TTWorkContextSessionMap = {
+ '1': { '2023-01-01': { s: 1, e: 2, b: 3, bt: 4 } },
+ };
+ const archiveYoung: TTWorkContextSessionMap = {
+ '1': { '2023-01-01': { s: 5, e: 6, b: 7, bt: 8 } },
+ };
+ const archiveOld: TTWorkContextSessionMap = {
+ '1': { '2023-01-01': { s: 9, e: 10, b: 11, bt: 12 } },
+ };
+
+ const result = mergeTimeTrackingStatesForWorkContext({
+ current,
+ archiveYoung: archiveYoung,
+ archiveOld: archiveOld,
+ });
+
+ expect(result).toEqual({ 1: { '2023-01-01': { s: 1, e: 2, b: 3, bt: 4 } } });
+ });
+
+ it('should handle missing dates and contexts correctly', () => {
+ const current: TTWorkContextSessionMap = {
+ '1': { '2023-01-01': { s: 1, e: 2, b: 3, bt: 4 } },
+ };
+ const archiveYoung: TTWorkContextSessionMap = {
+ '2': { '2023-01-02': { s: 5, e: 6, b: 7, bt: 8 } },
+ };
+ const archiveOld: TTWorkContextSessionMap = {
+ '3': { '2023-01-03': { s: 9, e: 10, b: 11, bt: 12 } },
+ };
+
+ const result = mergeTimeTrackingStatesForWorkContext({
+ current,
+ archiveYoung: archiveYoung,
+ archiveOld: archiveOld,
+ });
+
+ expect(result).toEqual({
+ 1: { '2023-01-01': { b: 3, bt: 4, e: 2, s: 1 } },
+ 2: { '2023-01-02': { b: 7, bt: 8, e: 6, s: 5 } },
+ 3: { '2023-01-03': { b: 11, bt: 12, e: 10, s: 9 } },
+ });
+ });
+
+ it('should not create empty context', () => {
+ const current: TTWorkContextSessionMap = {
+ '1': {},
+ };
+ const archiveYoung: TTWorkContextSessionMap = {
+ '2': {},
+ };
+ const archiveOld: TTWorkContextSessionMap = {
+ '3': { '2023-01-03': { s: 9, e: 10, b: 11, bt: 12 } },
+ };
+
+ const result = mergeTimeTrackingStatesForWorkContext({
+ current,
+ archiveYoung: archiveYoung,
+ archiveOld: archiveOld,
+ });
+
+ expect(result).toEqual({
+ 3: { '2023-01-03': { b: 11, bt: 12, e: 10, s: 9 } },
+ });
+ });
+
+ it('should not create empty entries for dates', () => {
+ const current: TTWorkContextSessionMap = {
+ '1': { '2023-01-01': {} },
+ };
+ const archiveYoung: TTWorkContextSessionMap = {
+ '2': { '2023-01-02': {} },
+ };
+ const archiveOld: TTWorkContextSessionMap = {
+ '3': { '2023-01-03': { s: 9, e: 10, b: 11, bt: 12 } },
+ };
+
+ const result = mergeTimeTrackingStatesForWorkContext({
+ current,
+ archiveYoung: archiveYoung,
+ archiveOld: archiveOld,
+ });
+
+ expect(result).toEqual({
+ 3: { '2023-01-03': { b: 11, bt: 12, e: 10, s: 9 } },
+ });
+ });
+});
diff --git a/src/app/features/time-tracking/merge-time-tracking-states.ts b/src/app/features/time-tracking/merge-time-tracking-states.ts
new file mode 100644
index 00000000000..3bbe090ebd1
--- /dev/null
+++ b/src/app/features/time-tracking/merge-time-tracking-states.ts
@@ -0,0 +1,85 @@
+import { TimeTrackingState, TTWorkContextSessionMap } from './time-tracking.model';
+
+export const mergeTimeTrackingStates = ({
+ current,
+ archiveYoung,
+ archiveOld,
+}: {
+ current: TimeTrackingState;
+ archiveYoung: TimeTrackingState;
+ archiveOld: TimeTrackingState;
+}): TimeTrackingState => {
+ return {
+ project: mergeTimeTrackingStatesForWorkContext({
+ current: current.project,
+ archiveYoung: archiveYoung.project,
+ archiveOld: archiveOld.project,
+ }),
+ tag: mergeTimeTrackingStatesForWorkContext({
+ current: current.tag,
+ archiveYoung: archiveYoung.tag,
+ archiveOld: archiveOld.tag,
+ }),
+ // lastFlush: current.lastFlush,
+ // task: current.task,
+ };
+};
+
+/**
+ * Merges time tracking data from three sources with priority: current > archiveYoung > oldArchive
+ * WARNING: Performance-intensive operation, use sparingly!
+ */
+export const mergeTimeTrackingStatesForWorkContext = ({
+ current,
+ archiveYoung,
+ archiveOld,
+}: {
+ current: TTWorkContextSessionMap;
+ archiveYoung: TTWorkContextSessionMap;
+ archiveOld: TTWorkContextSessionMap;
+}): TTWorkContextSessionMap => {
+ const result: TTWorkContextSessionMap = {};
+
+ // Get all unique work context IDs from all three sources
+ const allContextIds = new Set([
+ ...Object.keys(archiveOld || {}),
+ ...Object.keys(archiveYoung || {}),
+ ...Object.keys(current || {}),
+ ]);
+
+ // For each work context ID
+ for (const contextId of allContextIds) {
+ const archiveOldDates = archiveOld?.[contextId] || {};
+ const archiveYoungDates = archiveYoung?.[contextId] || {};
+ const currentDates = current?.[contextId] || {};
+
+ // Get all unique dates for this context
+ const allDates = Array.from(
+ new Set([
+ ...Object.keys(archiveOldDates),
+ ...Object.keys(archiveYoungDates),
+ ...Object.keys(currentDates),
+ ]),
+ );
+
+ if (allDates.length === 0) {
+ continue;
+ }
+
+ for (const date of allDates) {
+ const newData = {
+ // Merge in order of priority: current > archiveYoung > archiveOld
+ ...archiveOldDates[date],
+ ...archiveYoungDates[date],
+ ...currentDates[date],
+ };
+ if (Object.keys(newData).length > 0) {
+ if (!result[contextId]) {
+ result[contextId] = {};
+ }
+ result[contextId][date] = newData;
+ }
+ }
+ }
+ return result;
+};
diff --git a/src/app/features/time-tracking/sort-data-to-flush.spec.ts b/src/app/features/time-tracking/sort-data-to-flush.spec.ts
new file mode 100644
index 00000000000..fea164462f9
--- /dev/null
+++ b/src/app/features/time-tracking/sort-data-to-flush.spec.ts
@@ -0,0 +1,467 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import {
+ sortTimeTrackingAndTasksFromArchiveYoungToOld,
+ sortTimeTrackingDataToArchiveYoung,
+ splitArchiveTasksByDoneOnThreshold,
+} from './sort-data-to-flush';
+import {
+ ArchiveModel,
+ TimeTrackingState,
+ TTWorkContextData,
+} from './time-tracking.model';
+import { ImpossibleError } from '../../pfapi/api';
+import { TaskCopy } from '../tasks/task.model';
+
+const BASE_TASK: TaskCopy = {
+ title: 'base task',
+ subTaskIds: [],
+ timeSpentOnDay: {},
+ timeSpent: 0,
+ timeEstimate: 0,
+ isDone: true,
+ notes: '',
+ tagIds: [],
+ created: 0,
+ _hideSubTasksMode: 2,
+ attachments: [],
+} as Partial as TaskCopy;
+
+describe('sort-data-to-flush', () => {
+ // Mock the getWorklogStr function using Jasmine
+ const MOCK_TODAY_STR = '2023-05-15';
+
+ // Helper to create a valid TTWorkContextData object
+ const createTimeEntry = (): TTWorkContextData => ({
+ s: 1, // start time
+ e: 2, // end time
+ b: 3, // break number
+ bt: 4, // break time
+ });
+
+ describe('sortTimeTrackingDataToArchiveYoung()', () => {
+ it('should move old time tracking entries to archive', () => {
+ // Arrange
+ const timeTrackingIn: TimeTrackingState = {
+ project: {
+ project1: {
+ '2023-05-14': createTimeEntry(),
+ '2023-05-15': createTimeEntry(),
+ },
+ project2: {
+ '2023-05-13': createTimeEntry(),
+ },
+ },
+ tag: {
+ tag1: {
+ '2023-05-14': createTimeEntry(),
+ '2023-05-15': createTimeEntry(),
+ },
+ },
+ };
+
+ const archiveYoung: ArchiveModel = {
+ task: { ids: [], entities: {} },
+ timeTracking: {
+ project: {
+ project3: {
+ '2023-05-10': createTimeEntry(),
+ },
+ },
+ tag: {},
+ },
+ lastTimeTrackingFlush: 1621000000000,
+ };
+
+ // Act
+ const result = sortTimeTrackingDataToArchiveYoung({
+ timeTracking: timeTrackingIn,
+ archiveYoung,
+ todayStr: '2023-05-15',
+ });
+ console.log(result);
+
+ // values for today should still be there
+ expect(result.timeTracking.project.project1['2023-05-15']).toBeDefined();
+ expect(result.timeTracking.tag.tag1['2023-05-15']).toBeDefined();
+
+ // everything else should be gone
+ expect(result.timeTracking.project.project1['2023-05-14']).toBeUndefined();
+ expect(result.timeTracking.project.project2['2023-05-13']).toBeUndefined();
+ expect(result.timeTracking.tag.tag1['2023-05-14']).toBeUndefined();
+
+ expect(
+ result.archiveYoung.timeTracking.project.project1['2023-05-14'],
+ ).toBeDefined();
+ expect(
+ result.archiveYoung.timeTracking.project.project2['2023-05-13'],
+ ).toBeDefined();
+ expect(
+ result.archiveYoung.timeTracking.project.project3['2023-05-10'],
+ ).toBeDefined();
+ expect(result.archiveYoung.timeTracking.tag.tag1['2023-05-14']).toBeDefined();
+ });
+
+ it('should handle empty time tracking data', () => {
+ // Arrange
+ const timeTracking: TimeTrackingState = {
+ project: {},
+ tag: {},
+ };
+
+ const archiveYoung: ArchiveModel = {
+ task: { ids: [], entities: {} },
+ timeTracking: { project: {}, tag: {} },
+ lastTimeTrackingFlush: 1621000000000,
+ };
+
+ // Act
+ const result = sortTimeTrackingDataToArchiveYoung({
+ timeTracking,
+ archiveYoung,
+ todayStr: MOCK_TODAY_STR,
+ });
+
+ // Assert
+ expect(result.timeTracking).toEqual({ project: {}, tag: {} });
+ expect(result.archiveYoung.timeTracking).toEqual({ project: {}, tag: {} });
+ });
+ });
+
+ describe('splitArchiveTasksByDoneOnThreshold()', () => {
+ const now = 1621100000000; // May 16, 2021
+ const threshold = 7 * 24 * 60 * 60 * 1000; // 7 days
+
+ it('should move tasks older than threshold', () => {
+ // Arrange
+ const youngTaskState = {
+ ids: ['1', '2', '3'],
+ entities: {
+ '1': {
+ ...BASE_TASK,
+ id: '1',
+ doneOn: now - (threshold + 1000),
+ },
+ '2': {
+ ...BASE_TASK,
+ id: '2',
+ doneOn: now - (threshold - 3000),
+ },
+ '3': {
+ ...BASE_TASK,
+ id: '3',
+ doneOn: now - (threshold + 5000),
+ },
+ },
+ };
+
+ const oldTaskState = {
+ ids: ['4'],
+ entities: {
+ '4': {
+ ...BASE_TASK,
+ id: '4',
+ // eslint-disable-next-line no-mixed-operators
+ doneOn: now - threshold * 2,
+ },
+ },
+ };
+
+ // Act
+ const result = splitArchiveTasksByDoneOnThreshold({
+ youngTaskState,
+ oldTaskState,
+ threshold,
+ now,
+ });
+
+ // Assert
+ expect(Object.keys(result.youngTaskState.entities)).toEqual(['2']);
+ expect(Object.keys(result.oldTaskState.entities).sort()).toEqual(['1', '3', '4']);
+ });
+
+ it('should keep tasks newer than threshold', () => {
+ // Arrange
+ const youngTaskState = {
+ ids: ['1', '2'],
+ entities: {
+ '1': {
+ ...BASE_TASK,
+ id: '1',
+ doneOn: now - 1000,
+ },
+ '2': {
+ ...BASE_TASK,
+ id: '2',
+ doneOn: now - 2000,
+ },
+ },
+ };
+
+ const oldTaskState = {
+ ids: ['3'],
+ entities: {
+ '3': {
+ ...BASE_TASK,
+ id: '3',
+ // eslint-disable-next-line no-mixed-operators
+ doneOn: now - threshold * 2,
+ },
+ },
+ };
+
+ // Act
+ const result = splitArchiveTasksByDoneOnThreshold({
+ youngTaskState,
+ oldTaskState,
+ threshold,
+ now,
+ });
+
+ // Assert
+ expect(Object.keys(result.youngTaskState.entities)).toEqual(['1', '2']);
+ expect(Object.keys(result.oldTaskState.entities)).toEqual(['3']);
+ });
+
+ it('should migrate parent and sub tasks together', () => {
+ // Arrange
+ const youngTaskState = {
+ ids: ['1', '2'],
+ entities: {
+ '1': {
+ ...BASE_TASK,
+ id: '1',
+ doneOn: now - 1000,
+ },
+ '2P': {
+ ...BASE_TASK,
+ id: '2P',
+ // eslint-disable-next-line no-mixed-operators
+ doneOn: now - threshold * 2,
+ subTaskIds: ['3S', '4S'],
+ },
+ '3S': {
+ ...BASE_TASK,
+ id: '3S',
+ parentId: '2P',
+ doneOn: now - 2000,
+ },
+ '4S': {
+ ...BASE_TASK,
+ id: '4S',
+ parentId: '2P',
+ doneOn: now - 2000,
+ },
+ },
+ };
+
+ const oldTaskState = {
+ ids: ['5'],
+ entities: {
+ '5': {
+ ...BASE_TASK,
+ id: '5',
+ // eslint-disable-next-line no-mixed-operators
+ doneOn: now - threshold * 2,
+ },
+ },
+ };
+
+ // Act
+ const result = splitArchiveTasksByDoneOnThreshold({
+ youngTaskState,
+ oldTaskState,
+ threshold,
+ now,
+ });
+
+ // Assert
+ expect(Object.keys(result.youngTaskState.entities)).toEqual(['1']);
+ expect(Object.keys(result.oldTaskState.entities).sort()).toEqual([
+ '2P',
+ '3S',
+ '4S',
+ '5',
+ ]);
+ });
+
+ it('should also migrate legacy tasks without doneOn', () => {
+ // Arrange
+ const youngTaskState = {
+ ids: ['1'],
+ entities: {
+ '1': {
+ ...BASE_TASK,
+ id: '1',
+ doneOn: undefined,
+ },
+ },
+ };
+
+ const oldTaskState = {
+ ids: ['5'],
+ entities: {
+ '5': {
+ ...BASE_TASK,
+ id: '5',
+ // eslint-disable-next-line no-mixed-operators
+ doneOn: now - threshold * 2,
+ },
+ },
+ };
+
+ // Act
+ const result = splitArchiveTasksByDoneOnThreshold({
+ youngTaskState,
+ oldTaskState,
+ threshold,
+ now,
+ });
+
+ // Assert
+ expect(Object.keys(result.youngTaskState.entities)).toEqual([]);
+ expect(Object.keys(result.oldTaskState.entities).sort()).toEqual(['1', '5']);
+ });
+
+ it('should throw error if task is undefined', () => {
+ // Arrange
+ const youngTaskState = {
+ ids: ['1', '2'],
+ entities: {
+ '1': {
+ ...BASE_TASK,
+ id: '1',
+ doneOn: now - 1000,
+ },
+ '2': undefined as any,
+ },
+ };
+
+ const oldTaskState = {
+ ids: [],
+ entities: {},
+ };
+
+ // Act & Assert
+ expect(() => {
+ splitArchiveTasksByDoneOnThreshold({
+ youngTaskState,
+ oldTaskState,
+ threshold,
+ now,
+ });
+ }).toThrow(
+ new ImpossibleError('splitArchiveTasksByDoneOnThreshold(): Task not found'),
+ );
+ });
+ });
+
+ describe('sortTimeTrackingAndTasksFromArchiveYoungToOld', () => {
+ const now = 1621100000000; // May 16, 2021
+ const threshold = 7 * 24 * 60 * 60 * 1000; // 7 days
+
+ it('should move old tasks and all time tracking data', () => {
+ // Arrange
+ const archiveYoung: ArchiveModel = {
+ task: {
+ ids: ['1', '2'],
+ entities: {
+ '1': {
+ ...BASE_TASK,
+ id: '1',
+ doneOn: now - (threshold + 1000),
+ },
+ '2': {
+ ...BASE_TASK,
+ id: '2',
+ doneOn: now - 1000,
+ },
+ },
+ },
+ timeTracking: {
+ project: {
+ project1: { '2023-05-14': createTimeEntry() },
+ },
+ tag: {
+ tag1: { '2023-05-14': createTimeEntry() },
+ },
+ },
+ // eslint-disable-next-line no-mixed-operators
+ lastTimeTrackingFlush: now - threshold / 2,
+ };
+
+ const archiveOld: ArchiveModel = {
+ task: {
+ ids: ['3'],
+ entities: {
+ '3': {
+ ...BASE_TASK,
+ id: '3',
+ // eslint-disable-next-line no-mixed-operators
+ doneOn: now - threshold * 2,
+ },
+ },
+ },
+ timeTracking: {
+ project: {
+ project2: { '2023-05-10': createTimeEntry() },
+ },
+ tag: {},
+ },
+ lastTimeTrackingFlush: now - threshold,
+ };
+
+ // Act
+ const result = sortTimeTrackingAndTasksFromArchiveYoungToOld({
+ archiveYoung,
+ archiveOld,
+ threshold,
+ now,
+ });
+
+ // Assert
+ // Check tasks were moved correctly
+ expect(Object.keys(result.archiveYoung.task.entities)).toEqual(['2']);
+ expect(Object.keys(result.archiveOld.task.entities).sort()).toEqual(['1', '3']);
+
+ // Check all time tracking was moved to old
+ expect(result.archiveYoung.timeTracking).toEqual({ project: {}, tag: {} });
+ expect(result.archiveOld.timeTracking).toEqual({
+ project: {
+ project1: { '2023-05-14': createTimeEntry() },
+ project2: { '2023-05-10': createTimeEntry() },
+ },
+ tag: { tag1: { '2023-05-14': createTimeEntry() } },
+ });
+ });
+
+ it('should handle empty archives', () => {
+ // Arrange
+ const archiveYoung: ArchiveModel = {
+ task: { ids: [], entities: {} },
+ timeTracking: { project: {}, tag: {} },
+ // eslint-disable-next-line no-mixed-operators
+ lastTimeTrackingFlush: now - threshold / 2,
+ };
+
+ const archiveOld: ArchiveModel = {
+ task: { ids: [], entities: {} },
+ timeTracking: { project: {}, tag: {} },
+ lastTimeTrackingFlush: now - threshold,
+ };
+
+ // Act
+ const result = sortTimeTrackingAndTasksFromArchiveYoungToOld({
+ archiveYoung,
+ archiveOld,
+ threshold,
+ now,
+ });
+
+ // Assert
+ expect(result.archiveYoung.task.ids).toEqual([]);
+ expect(result.archiveOld.task.ids).toEqual([]);
+ expect(result.archiveYoung.timeTracking).toEqual({ project: {}, tag: {} });
+ expect(result.archiveOld.timeTracking).toEqual({ project: {}, tag: {} });
+ });
+ });
+});
diff --git a/src/app/features/time-tracking/sort-data-to-flush.ts b/src/app/features/time-tracking/sort-data-to-flush.ts
new file mode 100644
index 00000000000..8a353cc606e
--- /dev/null
+++ b/src/app/features/time-tracking/sort-data-to-flush.ts
@@ -0,0 +1,190 @@
+import {
+ ArchiveModel,
+ TimeTrackingState,
+ TTWorkContextSessionMap,
+} from './time-tracking.model';
+import { ArchiveTask, TaskArchive } from '../tasks/task.model';
+import { ImpossibleError } from '../../pfapi/api';
+
+const TIME_TRACKING_CATEGORIES = ['project', 'tag'] as const;
+
+export const sortTimeTrackingDataToArchiveYoung = ({
+ timeTracking,
+ archiveYoung,
+ todayStr,
+}: {
+ timeTracking: TimeTrackingState;
+ archiveYoung: ArchiveModel;
+ todayStr: string;
+}): {
+ timeTracking: TimeTrackingState;
+ archiveYoung: ArchiveModel;
+} => {
+ const currTT = { ...timeTracking };
+ const archiveTT = { ...archiveYoung.timeTracking };
+
+ // Find dates that are not today and move them to archive
+ // First iterate over categories (project, tag)
+ TIME_TRACKING_CATEGORIES.forEach((category) => {
+ if (!currTT[category]) currTT[category] = {};
+ if (!archiveTT[category]) archiveTT[category] = {};
+
+ // Then iterate over each project/tag within the category
+ Object.keys(timeTracking[category]).forEach((contextId) => {
+ if (!currTT[category][contextId]) currTT[category][contextId] = {};
+ if (!archiveTT[category][contextId]) archiveTT[category][contextId] = {};
+
+ // Finally iterate over dates for each project/tag
+ Object.keys(timeTracking[category][contextId]).forEach((dateStr) => {
+ if (dateStr !== todayStr) {
+ // Move to archive
+ if (!archiveTT[category][contextId]) {
+ archiveTT[category][contextId] = {};
+ }
+ archiveTT[category][contextId][dateStr] =
+ timeTracking[category][contextId][dateStr];
+ delete currTT[category][contextId][dateStr];
+ }
+ });
+ });
+ });
+
+ return {
+ timeTracking: currTT,
+ archiveYoung: {
+ ...archiveYoung,
+ timeTracking: archiveTT,
+ },
+ };
+};
+
+export const sortTimeTrackingAndTasksFromArchiveYoungToOld = ({
+ archiveYoung,
+ archiveOld,
+ threshold,
+ now,
+}: {
+ archiveYoung: ArchiveModel;
+ archiveOld: ArchiveModel;
+ threshold: number;
+ now: number;
+}): {
+ archiveYoung: ArchiveModel;
+ archiveOld: ArchiveModel;
+} => {
+ // Sort tasks based on doneOn threshold
+ const { youngTaskState, oldTaskState } = splitArchiveTasksByDoneOnThreshold({
+ youngTaskState: archiveYoung.task,
+ oldTaskState: archiveOld.task,
+ now,
+ threshold,
+ });
+
+ // Move all timeTracking data from young to old archive
+ // Deep merge time tracking data from young to old archive
+ const mergedTimeTracking = {
+ project: mergeTimeTrackingCategory(
+ archiveYoung.timeTracking.project,
+ archiveOld.timeTracking.project,
+ ),
+ tag: mergeTimeTrackingCategory(
+ archiveYoung.timeTracking.tag,
+ archiveOld.timeTracking.tag,
+ ),
+ };
+
+ return {
+ archiveYoung: {
+ ...archiveYoung,
+ task: youngTaskState,
+ // Clear timeTracking data from young archive
+ timeTracking: {
+ project: {},
+ tag: {},
+ },
+ },
+ archiveOld: {
+ ...archiveOld,
+ task: oldTaskState,
+ timeTracking: mergedTimeTracking,
+ },
+ };
+};
+
+const mergeTimeTrackingCategory = (
+ source: TTWorkContextSessionMap,
+ target: TTWorkContextSessionMap,
+): TTWorkContextSessionMap => {
+ const result = { ...target };
+
+ Object.entries(source || {}).forEach(([contextId, contextData]) => {
+ if (!result[contextId]) {
+ result[contextId] = {};
+ }
+
+ Object.entries(contextData).forEach(([dateStr, entry]) => {
+ result[contextId][dateStr] = entry;
+ });
+ });
+
+ return result;
+};
+
+export const splitArchiveTasksByDoneOnThreshold = ({
+ youngTaskState,
+ oldTaskState,
+ threshold,
+ now,
+}: {
+ youngTaskState: TaskArchive;
+ oldTaskState: TaskArchive;
+ threshold: number;
+ now: number;
+}): {
+ youngTaskState: TaskArchive;
+ oldTaskState: TaskArchive;
+} => {
+ // Find tasks that should be moved to old archive (doneOn < threshold)
+ const tasksToMove = Object.values(youngTaskState.entities).filter((task) => {
+ if (!task) {
+ throw new ImpossibleError('splitArchiveTasksByDoneOnThreshold(): Task not found');
+ }
+ // NOTE: we also need to consider legacy tasks without doneOn
+ return !task.parentId && (task.doneOn ? now - task.doneOn > threshold : true);
+ }) as ArchiveTask[];
+
+ // Exit early if no tasks to move
+ if (tasksToMove.length === 0) {
+ return { youngTaskState, oldTaskState };
+ }
+
+ // Create new states
+ const newYoungEntities = { ...youngTaskState.entities };
+ const newOldEntities = { ...oldTaskState.entities };
+
+ // Move tasks to old archive
+ tasksToMove.forEach((task) => {
+ delete newYoungEntities[task.id];
+ newOldEntities[task.id] = task;
+ if (task.subTaskIds) {
+ task.subTaskIds.forEach((subTaskId) => {
+ const subTask = newYoungEntities[subTaskId];
+ if (subTask) {
+ delete newYoungEntities[subTaskId];
+ newOldEntities[subTaskId] = subTask;
+ }
+ });
+ }
+ });
+
+ return {
+ youngTaskState: {
+ ids: Object.keys(newYoungEntities),
+ entities: newYoungEntities,
+ },
+ oldTaskState: {
+ ids: Object.keys(newOldEntities),
+ entities: newOldEntities,
+ },
+ };
+};
diff --git a/src/app/features/time-tracking/store/time-tracking.actions.ts b/src/app/features/time-tracking/store/time-tracking.actions.ts
new file mode 100644
index 00000000000..7d0f94d5ca9
--- /dev/null
+++ b/src/app/features/time-tracking/store/time-tracking.actions.ts
@@ -0,0 +1,25 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import { createActionGroup, props } from '@ngrx/store';
+import { WorkContextType } from '../../work-context/work-context.model';
+import { TimeTrackingState, TTWorkContextData } from '../time-tracking.model';
+import { Task } from '../../tasks/task.model';
+
+export const TimeTrackingActions = createActionGroup({
+ source: 'TimeTracking',
+ events: {
+ 'Update Work Context Data': props<{
+ ctx: { id: string; type: WorkContextType };
+ date: string;
+ updates: Partial;
+ }>(),
+ 'Add time spent': props<{
+ task: Task;
+ date: string;
+ duration: number;
+ isFromTrackingReminder: boolean;
+ }>(),
+ 'Update whole State': props<{
+ newState: TimeTrackingState;
+ }>(),
+ },
+});
diff --git a/src/app/imex/sync/dropbox/store/dropbox.effects.spec.ts b/src/app/features/time-tracking/store/time-tracking.effects.spec.ts
similarity index 57%
rename from src/app/imex/sync/dropbox/store/dropbox.effects.spec.ts
rename to src/app/features/time-tracking/store/time-tracking.effects.spec.ts
index 75d37eea08b..e4b60558284 100644
--- a/src/app/imex/sync/dropbox/store/dropbox.effects.spec.ts
+++ b/src/app/features/time-tracking/store/time-tracking.effects.spec.ts
@@ -2,21 +2,18 @@
// import { provideMockActions } from '@ngrx/effects/testing';
// import { Observable } from 'rxjs';
//
-// import { DropboxEffects } from './dropbox.effects';
+// import { TimeTrackingEffects } from './time-tracking.effects';
//
-// describe('DropboxEffects', () => {
+// describe('TimeTrackingEffects', () => {
// let actions$: Observable;
-// let effects: DropboxEffects;
+// let effects: TimeTrackingEffects;
//
// beforeEach(() => {
// TestBed.configureTestingModule({
-// providers: [
-// DropboxEffects,
-// provideMockActions(() => actions$)
-// ]
+// providers: [TimeTrackingEffects, provideMockActions(() => actions$)],
// });
//
-// effects = TestBed.inject(DropboxEffects);
+// effects = TestBed.inject(TimeTrackingEffects);
// });
//
// it('should be created', () => {
diff --git a/src/app/features/time-tracking/store/time-tracking.effects.ts b/src/app/features/time-tracking/store/time-tracking.effects.ts
new file mode 100644
index 00000000000..f625038bf28
--- /dev/null
+++ b/src/app/features/time-tracking/store/time-tracking.effects.ts
@@ -0,0 +1,54 @@
+import { inject, Injectable } from '@angular/core';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+
+import { auditTime, switchMap, take } from 'rxjs/operators';
+import { Observable } from 'rxjs';
+import { select, Store } from '@ngrx/store';
+import { PfapiService } from '../../../pfapi/pfapi.service';
+import { selectTimeTrackingState } from './time-tracking.selectors';
+import { TimeTrackingActions } from './time-tracking.actions';
+import { TIME_TRACKING_TO_DB_INTERVAL } from '../../../app.constants';
+
+@Injectable()
+export class TimeTrackingEffects {
+ private _actions$ = inject(Actions);
+ private _store$ = inject>(Store);
+ private _pfapiService = inject(PfapiService);
+
+ saveToLs$: Observable = this._store$.pipe(
+ select(selectTimeTrackingState),
+ take(1),
+ switchMap((ttState) =>
+ this._pfapiService.m.timeTracking.save(ttState, {
+ isUpdateRevAndLastUpdate: true,
+ }),
+ ),
+ );
+
+ updateTimeTrackingStorageAuditTime$: any = createEffect(
+ () =>
+ this._actions$.pipe(
+ ofType(
+ // TIME TRACKING
+ TimeTrackingActions.addTimeSpent,
+ ),
+ auditTime(TIME_TRACKING_TO_DB_INTERVAL),
+ switchMap(() => this.saveToLs$),
+ ),
+ { dispatch: false },
+ );
+
+ updateTimeTrackingStorage$: Observable = createEffect(
+ () =>
+ this._actions$.pipe(
+ ofType(
+ TimeTrackingActions.updateWorkContextData,
+ TimeTrackingActions.updateWholeState,
+ ),
+ switchMap(() => this.saveToLs$),
+ ),
+ { dispatch: false },
+ );
+
+ constructor() {}
+}
diff --git a/src/app/features/time-tracking/store/time-tracking.reducer.spec.ts b/src/app/features/time-tracking/store/time-tracking.reducer.spec.ts
new file mode 100644
index 00000000000..77233a5fed7
--- /dev/null
+++ b/src/app/features/time-tracking/store/time-tracking.reducer.spec.ts
@@ -0,0 +1,87 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import { initialTimeTrackingState, timeTrackingReducer } from './time-tracking.reducer';
+import { TimeTrackingActions } from './time-tracking.actions';
+import { loadAllData } from '../../../root-store/meta/load-all-data.action';
+import { AppDataCompleteNew } from '../../../pfapi/pfapi-config';
+import { TaskCopy } from '../../tasks/task.model';
+import { WorkContextType } from '../../work-context/work-context.model';
+
+describe('TimeTracking Reducer', () => {
+ it('should return the previous state for an unknown action', () => {
+ const action = {} as any;
+ const result = timeTrackingReducer(initialTimeTrackingState, action);
+ expect(result).toBe(initialTimeTrackingState);
+ });
+
+ it('should load all data', () => {
+ const appDataComplete: AppDataCompleteNew = {
+ timeTracking: {
+ project: { '1': { '2023-01-01': { s: 1, e: 2, b: 3, bt: 4 } } },
+ tag: { '2': { '2023-01-02': { s: 5, e: 6, b: 7, bt: 8 } } },
+ },
+ } as Partial as AppDataCompleteNew;
+ const action = loadAllData({ appDataComplete, isOmitTokens: true });
+ const result = timeTrackingReducer(initialTimeTrackingState, action);
+ expect(result).toEqual(appDataComplete.timeTracking);
+ });
+
+ it('should update the whole state', () => {
+ const newState = {
+ project: { '1': { '2023-01-01': { s: 1, e: 2, b: 3, bt: 4 } } },
+ tag: { '2': { '2023-01-02': { s: 5, e: 6, b: 7, bt: 8 } } },
+ };
+ const action = TimeTrackingActions.updateWholeState({ newState });
+ const result = timeTrackingReducer(initialTimeTrackingState, action);
+ expect(result).toEqual(newState);
+ });
+
+ it('should add time spent', () => {
+ const task = { projectId: '1', tagIds: ['2'] } as Partial as TaskCopy;
+ const date = '2023-01-01';
+ const action = TimeTrackingActions.addTimeSpent({
+ task,
+ date,
+ duration: 223,
+ isFromTrackingReminder: false,
+ });
+ const result = timeTrackingReducer(initialTimeTrackingState, action);
+ expect(result.project['1'][date].s).toBeDefined();
+ expect(result.project['1'][date].e).toBeDefined();
+ expect(result.tag['2'][date].s).toBeDefined();
+ expect(result.tag['2'][date].e).toBeDefined();
+ });
+
+ it('should update work context data', () => {
+ // Set up a custom initial state with the required structure
+ const customInitialState = {
+ ...initialTimeTrackingState,
+ project: { '1': { '2023-01-01': { s: 0, e: 0 } } },
+ };
+
+ const ctx = { id: '1', type: 'PROJECT' as WorkContextType };
+ const date = '2023-01-01';
+ const updates = { s: 10, e: 20 };
+ const action = TimeTrackingActions.updateWorkContextData({ ctx, date, updates });
+ const result = timeTrackingReducer(customInitialState, action);
+
+ expect(result.project['1'][date].s).toBe(10);
+ expect(result.project['1'][date].e).toBe(20);
+ });
+
+ it('should update work context data for a tag', () => {
+ // Set up a custom initial state with the required structure
+ const customInitialState = {
+ ...initialTimeTrackingState,
+ tag: { '2': { '2023-01-02': { s: 0, e: 0 } } },
+ };
+
+ const ctx = { id: '2', type: 'TAG' as WorkContextType };
+ const date = '2023-01-02';
+ const updates = { s: 30, e: 40 };
+ const action = TimeTrackingActions.updateWorkContextData({ ctx, date, updates });
+ const result = timeTrackingReducer(customInitialState, action);
+
+ expect(result.tag['2'][date].s).toBe(30);
+ expect(result.tag['2'][date].e).toBe(40);
+ });
+});
diff --git a/src/app/features/time-tracking/store/time-tracking.reducer.ts b/src/app/features/time-tracking/store/time-tracking.reducer.ts
new file mode 100644
index 00000000000..37583570799
--- /dev/null
+++ b/src/app/features/time-tracking/store/time-tracking.reducer.ts
@@ -0,0 +1,91 @@
+import { TimeTrackingActions } from './time-tracking.actions';
+import { createFeature, createReducer, on } from '@ngrx/store';
+import { TimeTrackingState } from '../time-tracking.model';
+import { loadAllData } from '../../../root-store/meta/load-all-data.action';
+import { AppDataCompleteNew } from '../../../pfapi/pfapi-config';
+import { roundTsToMinutes } from '../../../util/round-ts-to-minutes';
+
+export const TIME_TRACKING_FEATURE_KEY = 'timeTracking' as const;
+
+// export const initialTimeTrackingState: TimeTrackingState = {
+export const initialTimeTrackingState: TimeTrackingState = {
+ tag: {},
+ project: {},
+ // lastFlush: 0,
+} as const;
+
+export const timeTrackingReducer = createReducer(
+ initialTimeTrackingState,
+
+ on(loadAllData, (state, { appDataComplete }) =>
+ (appDataComplete as AppDataCompleteNew).timeTracking
+ ? (appDataComplete as AppDataCompleteNew).timeTracking
+ : state,
+ ),
+ on(TimeTrackingActions.updateWholeState, (state, { newState }) => newState),
+
+ on(TimeTrackingActions.addTimeSpent, (state, { task, date }) => {
+ const isUpdateProject = !!task.projectId;
+ const isUpdateTags = task.tagIds && !!task.tagIds.length;
+ return {
+ ...state,
+ ...(isUpdateProject
+ ? {
+ project: {
+ ...state.project,
+ [task.projectId]: {
+ ...state.project[task.projectId],
+ [date]: {
+ ...state.project[task.projectId]?.[date],
+ e: roundTsToMinutes(Date.now()),
+ s: roundTsToMinutes(
+ state.project[task.projectId]?.[date]?.s || Date.now(),
+ ),
+ },
+ },
+ },
+ }
+ : {}),
+ ...(isUpdateTags
+ ? {
+ tag: {
+ ...state.tag,
+ ...(task.tagIds as string[]).reduce((acc, tagId) => {
+ acc[tagId] = {
+ ...state.tag[tagId],
+ [date]: {
+ ...state.tag[tagId]?.[date],
+ e: roundTsToMinutes(Date.now()),
+ s: roundTsToMinutes(state.tag[tagId]?.[date]?.s || Date.now()),
+ },
+ };
+ return acc;
+ }, {}),
+ },
+ }
+ : {}),
+ };
+ }),
+
+ on(TimeTrackingActions.updateWorkContextData, (state, { ctx, date, updates }) => {
+ const prop = ctx.type === 'TAG' ? 'tag' : 'project';
+ return {
+ ...state,
+ [prop]: {
+ ...state[prop],
+ [ctx.id]: {
+ ...state[prop][ctx.id],
+ [date]: {
+ ...state[prop][ctx.id][date],
+ ...updates,
+ },
+ },
+ },
+ };
+ }),
+);
+
+export const timeTrackingFeature = createFeature({
+ name: TIME_TRACKING_FEATURE_KEY,
+ reducer: timeTrackingReducer,
+});
diff --git a/src/app/features/time-tracking/store/time-tracking.selectors.spec.ts b/src/app/features/time-tracking/store/time-tracking.selectors.spec.ts
new file mode 100644
index 00000000000..7223cb8df25
--- /dev/null
+++ b/src/app/features/time-tracking/store/time-tracking.selectors.spec.ts
@@ -0,0 +1,12 @@
+// import * as fromTimeTracking from './time-tracking.reducer';
+// import { selectTimeTrackingState } from './time-tracking.selectors';
+//
+// describe('TimeTracking Selectors', () => {
+// it('should select the feature state', () => {
+// const result = selectTimeTrackingState({
+// [fromTimeTracking.TIME_TRACKING_FEATURE_KEY]: {},
+// });
+//
+// expect(result).toEqual({});
+// });
+// });
diff --git a/src/app/features/time-tracking/store/time-tracking.selectors.ts b/src/app/features/time-tracking/store/time-tracking.selectors.ts
new file mode 100644
index 00000000000..00942bd2e42
--- /dev/null
+++ b/src/app/features/time-tracking/store/time-tracking.selectors.ts
@@ -0,0 +1,7 @@
+import { createFeatureSelector } from '@ngrx/store';
+import * as fromTimeTracking from './time-tracking.reducer';
+import { TimeTrackingState } from '../time-tracking.model';
+
+export const selectTimeTrackingState = createFeatureSelector(
+ fromTimeTracking.TIME_TRACKING_FEATURE_KEY,
+);
diff --git a/src/app/features/time-tracking/task-archive.service.spec.ts b/src/app/features/time-tracking/task-archive.service.spec.ts
new file mode 100644
index 00000000000..4f470d54d39
--- /dev/null
+++ b/src/app/features/time-tracking/task-archive.service.spec.ts
@@ -0,0 +1,16 @@
+// import { TestBed } from '@angular/core/testing';
+//
+// import { TaskArchiveService } from './task-archive.service';
+//
+// describe('TaskArchiveService', () => {
+// let service: TaskArchiveService;
+//
+// beforeEach(() => {
+// TestBed.configureTestingModule({});
+// service = TestBed.inject(TaskArchiveService);
+// });
+//
+// it('should be created', () => {
+// expect(service).toBeTruthy();
+// });
+// });
diff --git a/src/app/features/time-tracking/task-archive.service.ts b/src/app/features/time-tracking/task-archive.service.ts
new file mode 100644
index 00000000000..03d4af6d231
--- /dev/null
+++ b/src/app/features/time-tracking/task-archive.service.ts
@@ -0,0 +1,343 @@
+import { inject, Injectable } from '@angular/core';
+import {
+ deleteTasks,
+ removeTagsForAllTasks,
+ roundTimeSpentForDay,
+ updateTask,
+} from '../tasks/store/task.actions';
+import { taskReducer } from '../tasks/store/task.reducer';
+import { PfapiService } from '../../pfapi/pfapi.service';
+import { Task, TaskArchive, TaskState } from '../tasks/task.model';
+import { RoundTimeOption } from '../project/project.model';
+import { Update } from '@ngrx/entity';
+import { ArchiveModel } from './time-tracking.model';
+
+type TaskArchiveAction =
+ | ReturnType
+ | ReturnType
+ | ReturnType
+ | ReturnType;
+
+@Injectable({
+ providedIn: 'root',
+})
+export class TaskArchiveService {
+ private _pfapiService = inject(PfapiService);
+
+ constructor() {}
+
+ async load(isSkipMigration = false): Promise {
+ // NOTE: these are already saved in memory to speed up things
+ const [archiveYoung, archiveOld] = await Promise.all([
+ this._pfapiService.m.archiveYoung.load(isSkipMigration),
+ this._pfapiService.m.archiveOld.load(isSkipMigration),
+ ]);
+
+ return {
+ ids: [...archiveYoung.task.ids, ...archiveOld.task.ids],
+ entities: {
+ ...archiveYoung.task.entities,
+ ...archiveOld.task.entities,
+ },
+ };
+ }
+
+ async getById(id: string): Promise {
+ const archiveYoung = await this._pfapiService.m.archiveYoung.load();
+ if (archiveYoung.task.entities[id]) {
+ return archiveYoung.task.entities[id];
+ }
+ const archiveOld = await this._pfapiService.m.archiveOld.load();
+ if (archiveOld.task.entities[id]) {
+ return archiveOld.task.entities[id];
+ }
+ throw new Error('Archive task not found by id');
+ }
+
+ async deleteTasks(taskIdsToDelete: string[]): Promise {
+ const archiveYoung = await this._pfapiService.m.archiveYoung.load();
+ const toDeleteInArchiveYoung = taskIdsToDelete.filter(
+ (id) => !!archiveYoung.task.entities[id],
+ );
+
+ if (toDeleteInArchiveYoung.length > 0) {
+ const newTaskState = taskReducer(
+ archiveYoung.task as TaskState,
+ deleteTasks({ taskIds: toDeleteInArchiveYoung }),
+ );
+ await this._pfapiService.m.archiveYoung.save(
+ {
+ ...archiveYoung,
+ task: newTaskState,
+ },
+ { isUpdateRevAndLastUpdate: true },
+ );
+ }
+
+ if (toDeleteInArchiveYoung.length < taskIdsToDelete.length) {
+ const archiveOld = await this._pfapiService.m.archiveOld.load();
+ const toDeleteInArchiveOld = taskIdsToDelete.filter(
+ (id) => !!archiveOld.task.entities[id],
+ );
+ const newTaskStateArchiveOld = taskReducer(
+ archiveOld.task as TaskState,
+ deleteTasks({ taskIds: toDeleteInArchiveOld }),
+ );
+ await this._pfapiService.m.archiveOld.save(
+ {
+ ...archiveOld,
+ task: newTaskStateArchiveOld,
+ },
+ { isUpdateRevAndLastUpdate: true },
+ );
+ }
+ }
+
+ async updateTask(id: string, changedFields: Partial): Promise {
+ const archiveYoung = await this._pfapiService.m.archiveYoung.load();
+ if (archiveYoung.task.entities[id]) {
+ return await this._execAction(
+ 'archive',
+ archiveYoung,
+ updateTask({ task: { id, changes: changedFields } }),
+ );
+ }
+ const archiveOld = await this._pfapiService.m.archiveOld.load();
+ if (archiveOld.task.entities[id]) {
+ return await this._execAction(
+ 'archiveOld',
+ archiveOld,
+ updateTask({ task: { id, changes: changedFields } }),
+ );
+ }
+ throw new Error('Archive task to update not found');
+ }
+
+ async updateTasks(updates: Update[]): Promise {
+ const allUpdates = updates.map((upd) => updateTask({ task: upd }));
+ const archiveYoung = await this._pfapiService.m.archiveYoung.load();
+ const updatesYoung = allUpdates.filter(
+ (upd) => !!archiveYoung.task.entities[upd.task.id],
+ );
+ if (updatesYoung.length > 0) {
+ const newTaskStateArchiveYoung = updatesYoung.reduce(
+ (acc, act) => taskReducer(acc, act),
+ archiveYoung.task as TaskState,
+ );
+ await this._pfapiService.m.archiveYoung.save(
+ {
+ ...archiveYoung,
+ task: newTaskStateArchiveYoung,
+ },
+ { isUpdateRevAndLastUpdate: true },
+ );
+ }
+
+ if (updatesYoung.length < updates.length) {
+ const archiveOld = await this._pfapiService.m.archiveOld.load();
+ const updatesOld = allUpdates.filter(
+ (upd) => !!archiveOld.task.entities[upd.task.id],
+ );
+ const newTaskStateArchiveOld = updatesOld.reduce(
+ (acc, act) => taskReducer(acc, act),
+ archiveOld.task as TaskState,
+ );
+ await this._pfapiService.m.archiveOld.save(
+ {
+ ...archiveOld,
+ task: newTaskStateArchiveOld,
+ },
+ { isUpdateRevAndLastUpdate: true },
+ );
+ }
+ }
+
+ // -----------------------------------------
+ async removeAllArchiveTasksForProject(projectIdToDelete: string): Promise {
+ const taskArchiveState: TaskArchive = await this.load();
+ const archiveTaskIdsToDelete = !!taskArchiveState
+ ? (taskArchiveState.ids as string[]).filter((id) => {
+ const t = taskArchiveState.entities[id] as Task;
+ if (!t) {
+ throw new Error('No task');
+ }
+ return t.projectId === projectIdToDelete;
+ })
+ : [];
+ await this.deleteTasks(archiveTaskIdsToDelete);
+ }
+
+ async removeTagsFromAllTasks(tagIdsToRemove: string[]): Promise {
+ const taskArchiveState: TaskArchive = await this.load();
+ await this._execActionBoth(removeTagsForAllTasks({ tagIdsToRemove }));
+
+ const isOrphanedParentTask = (t: Task): boolean =>
+ !t.projectId && !t.tagIds.length && !t.parentId;
+
+ // remove orphaned for archive
+
+ let archiveSubTaskIdsToDelete: string[] = [];
+ const archiveMainTaskIdsToDelete: string[] = [];
+ (taskArchiveState.ids as string[]).forEach((id) => {
+ const t = taskArchiveState.entities[id] as Task;
+ if (isOrphanedParentTask(t)) {
+ archiveMainTaskIdsToDelete.push(id);
+ archiveSubTaskIdsToDelete = archiveSubTaskIdsToDelete.concat(t.subTaskIds);
+ }
+ });
+ // TODO check to maybe update to today tag instead
+ await this.deleteTasks([...archiveMainTaskIdsToDelete, ...archiveSubTaskIdsToDelete]);
+ }
+
+ async removeRepeatCfgFromArchiveTasks(repeatConfigId: string): Promise {
+ const taskArchive = await this.load();
+
+ const newState = { ...taskArchive };
+ const ids = newState.ids as string[];
+
+ const tasksWithRepeatCfgId = ids
+ .map((id) => newState.entities[id] as Task)
+ .filter((task) => task.repeatCfgId === repeatConfigId);
+
+ if (tasksWithRepeatCfgId && tasksWithRepeatCfgId.length) {
+ const updates: Update[] = tasksWithRepeatCfgId.map((t) => {
+ return {
+ id: t.id,
+ changes: {
+ // TODO check if undefined causes problems
+ repeatCfgId: undefined,
+ },
+ };
+ });
+ await this.updateTasks(updates);
+ }
+ }
+
+ async roundTimeSpent({
+ day,
+ taskIds,
+ roundTo,
+ isRoundUp = false,
+ projectId,
+ }: {
+ day: string;
+ taskIds: string[];
+ roundTo: RoundTimeOption;
+ isRoundUp: boolean;
+ projectId?: string | null;
+ }): Promise {
+ const archiveYoung = await this._pfapiService.m.archiveYoung.load();
+ const taskIdsInArchiveYoung = taskIds.filter(
+ (id) => !!archiveYoung.task.entities[id],
+ );
+ if (taskIdsInArchiveYoung.length > 0) {
+ const newTaskState = taskReducer(
+ archiveYoung.task as TaskState,
+ roundTimeSpentForDay({
+ day,
+ taskIds: taskIdsInArchiveYoung,
+ roundTo,
+ isRoundUp,
+ projectId,
+ }),
+ );
+ await this._pfapiService.m.archiveYoung.save(
+ {
+ ...archiveYoung,
+ task: newTaskState,
+ },
+ { isUpdateRevAndLastUpdate: true },
+ );
+ }
+ if (taskIdsInArchiveYoung.length < taskIds.length) {
+ const archiveOld = await this._pfapiService.m.archiveOld.load();
+ const taskIdsInArchiveOld = taskIds.filter((id) => !!archiveOld.task.entities[id]);
+ if (taskIdsInArchiveOld.length > 0) {
+ const newTaskStateArchiveOld = taskReducer(
+ archiveOld.task as TaskState,
+ roundTimeSpentForDay({
+ day,
+ taskIds: taskIdsInArchiveOld,
+ roundTo,
+ isRoundUp,
+ projectId,
+ }),
+ );
+ await this._pfapiService.m.archiveOld.save(
+ {
+ ...archiveOld,
+ task: newTaskStateArchiveOld,
+ },
+ { isUpdateRevAndLastUpdate: true },
+ );
+ }
+ }
+ }
+
+ // -----------------------------------------
+
+ private async _execAction(
+ target: 'archive' | 'archiveOld',
+ archiveBefore: ArchiveModel,
+ action: TaskArchiveAction,
+ ): Promise {
+ const newTaskState = taskReducer(archiveBefore.task as TaskState, action);
+ await this._pfapiService.m[target].save(
+ {
+ ...archiveBefore,
+ task: newTaskState,
+ },
+ { isUpdateRevAndLastUpdate: true },
+ );
+ }
+
+ private async _execActionBoth(
+ action: TaskArchiveAction,
+ isUpdateRevAndLastUpdate = true,
+ ): Promise {
+ const archiveYoung = await this._pfapiService.m.archiveYoung.load();
+ const newTaskState = taskReducer(archiveYoung.task as TaskState, action);
+
+ const archiveOld = await this._pfapiService.m.archiveOld.load();
+ const newTaskStateArchiveOld = taskReducer(archiveOld.task as TaskState, action);
+
+ await this._pfapiService.m.archiveYoung.save(
+ {
+ ...archiveYoung,
+ task: newTaskState,
+ },
+ { isUpdateRevAndLastUpdate },
+ );
+ await this._pfapiService.m.archiveOld.save(
+ {
+ ...archiveOld,
+ task: newTaskStateArchiveOld,
+ },
+ { isUpdateRevAndLastUpdate },
+ );
+ }
+
+ // more beautiful but less efficient
+ // private async _partitionTasksByArchive(
+ // ids: string[],
+ // mapper: (id: string, archive: ArchiveModel) => T,
+ // ): Promise<{ young: T[]; old: T[] }> {
+ // const [archiveYoung, archiveOld] = await Promise.all([
+ // this._pfapiService.m.archive.load(),
+ // this._pfapiService.m.archiveOld.load(),
+ // ]);
+ //
+ // const young: T[] = [];
+ // const old: T[] = [];
+ //
+ // ids.forEach((id) => {
+ // if (archiveYoung.task.entities[id]) {
+ // young.push(mapper(id, archiveYoung));
+ // } else if (archiveOld.task.entities[id]) {
+ // old.push(mapper(id, archiveOld));
+ // }
+ // });
+ //
+ // return { young, old };
+ // }
+}
diff --git a/src/app/features/time-tracking/time-tracking.model.ts b/src/app/features/time-tracking/time-tracking.model.ts
new file mode 100644
index 00000000000..dd1dfc8930d
--- /dev/null
+++ b/src/app/features/time-tracking/time-tracking.model.ts
@@ -0,0 +1,69 @@
+// Base mapped types with clearer names
+import { TaskArchive } from '../tasks/task.model';
+
+export type TTModelId = string;
+export type TTDate = string;
+
+export type TTModelIdMap = Omit<
+ Record,
+ keyof TTWorkContextData | keyof TimeTrackingState | 'workStart' | 'workEnd'
+>;
+export type TTDateMap = Omit<
+ Record,
+ keyof TTWorkContextData | keyof TimeTrackingState | 'workStart' | 'workEnd'
+>;
+
+/**
+ * Time Tracking work context data
+ * Uses shortened property names to reduce storage size
+ * s: start time
+ * e: end time
+ * b: break number
+ * bt: break time
+ */
+export interface TTWorkContextData {
+ s?: number;
+ e?: number;
+ b?: number;
+ bt?: number;
+}
+
+/*
+project:
+ [projectId]:
+ [date]:
+ s: number;
+ ...
+
+project: -> TTWorkContextSessionMap
+ [projectId]: -> TTWorkSessionByDateMap
+ [date]: -> TTWorkContextData
+ s: number;
+ ...
+ */
+
+// Map of work session stats by date
+export type TTWorkSessionByDateMap = TTDateMap;
+
+// Work context (project/tag) mapped to their session data by date
+export type TTWorkContextSessionMap = TTModelIdMap;
+
+// Main state container
+export interface TimeTrackingState {
+ project: TTWorkContextSessionMap;
+ tag: TTWorkContextSessionMap;
+ // somehow can't be optional for ngrx
+}
+
+// Archive model
+export interface ArchiveModel {
+ // should not be written apart from flushing!
+ timeTracking: TimeTrackingState;
+ task: TaskArchive;
+ lastTimeTrackingFlush: number;
+}
+
+export const isWorkContextData = (obj: unknown): obj is TTWorkContextData =>
+ typeof obj === 'object' &&
+ obj !== null &&
+ ('s' in obj || 'e' in obj || 'b' in obj || 'bt' in obj);
diff --git a/src/app/features/time-tracking/time-tracking.service.spec.ts b/src/app/features/time-tracking/time-tracking.service.spec.ts
new file mode 100644
index 00000000000..f9cd87e3556
--- /dev/null
+++ b/src/app/features/time-tracking/time-tracking.service.spec.ts
@@ -0,0 +1,16 @@
+// import { TestBed } from '@angular/core/testing';
+//
+// import { TimeTrackingService } from './time-tracking.service';
+//
+// describe('TimeTrackingService', () => {
+// let service: TimeTrackingService;
+//
+// beforeEach(() => {
+// TestBed.configureTestingModule({});
+// service = TestBed.inject(TimeTrackingService);
+// });
+//
+// it('should be created', () => {
+// expect(service).toBeTruthy();
+// });
+// });
diff --git a/src/app/features/time-tracking/time-tracking.service.ts b/src/app/features/time-tracking/time-tracking.service.ts
new file mode 100644
index 00000000000..13cd3328eda
--- /dev/null
+++ b/src/app/features/time-tracking/time-tracking.service.ts
@@ -0,0 +1,158 @@
+import { inject, Injectable } from '@angular/core';
+import { combineLatest, Observable, Subject } from 'rxjs';
+import { TimeTrackingState, TTDateMap, TTWorkContextData } from './time-tracking.model';
+import { first, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
+import { mergeTimeTrackingStates } from './merge-time-tracking-states';
+import { Store } from '@ngrx/store';
+import { selectTimeTrackingState } from './store/time-tracking.selectors';
+import { PfapiService } from '../../pfapi/pfapi.service';
+import { WorkContextType, WorkStartEnd } from '../work-context/work-context.model';
+import { ImpossibleError } from '../../pfapi/api';
+import { toLegacyWorkStartEndMaps } from './to-legacy-work-start-end-maps';
+import { TimeTrackingActions } from './store/time-tracking.actions';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class TimeTrackingService {
+ private _store = inject(Store);
+ private _pfapiService = inject(PfapiService);
+
+ private _archiveYoungUpdateTrigger$ = new Subject();
+ private _archiveOldUpdateTrigger$ = new Subject();
+
+ current$: Observable = this._store.select(selectTimeTrackingState);
+ archiveYoung$: Observable = this._archiveYoungUpdateTrigger$.pipe(
+ startWith(null),
+ switchMap(async () => {
+ return (await this._pfapiService.m.archiveYoung.load()).timeTracking;
+ }),
+ shareReplay(1),
+ );
+
+ archiveOld$: Observable = this._archiveOldUpdateTrigger$.pipe(
+ startWith(null),
+ switchMap(async () => {
+ return (await this._pfapiService.m.archiveOld.load()).timeTracking;
+ }),
+ shareReplay(1),
+ );
+
+ state$: Observable = combineLatest([
+ this.current$,
+ this.archiveYoung$,
+ this.archiveOld$,
+ ]).pipe(
+ map(([current, archive, oldArchive]) =>
+ mergeTimeTrackingStates({ current, archiveYoung: archive, archiveOld: oldArchive }),
+ ),
+ shareReplay(1),
+ );
+
+ getWorkStartEndForWorkContext$(ctx: {
+ id: string;
+ type: WorkContextType;
+ }): Observable> {
+ const { id, type } = ctx;
+ return this.state$.pipe(
+ map((state) => {
+ if (type === 'PROJECT') {
+ return state.project[id] || ({} as TTDateMap);
+ }
+ if (type === 'TAG') {
+ return state.tag[id] || ({} as TTDateMap);
+ }
+ throw new ImpossibleError('Invalid work context type ' + type);
+ }),
+ );
+ }
+
+ async cleanupDataEverywhereForProject(projectId: string): Promise {
+ const current = await this.current$.pipe(first()).toPromise();
+ const archiveYoung = await this._pfapiService.m.archiveYoung.load();
+ const archiveOld = await this._pfapiService.m.archiveOld.load();
+
+ console.log({ current, archiveYoung, archiveOld });
+
+ if (projectId in current.project) {
+ const newProject = { ...current.project };
+ delete newProject[projectId];
+ this._store.dispatch(
+ TimeTrackingActions.updateWholeState({
+ newState: {
+ ...current,
+ project: { ...newProject },
+ },
+ }),
+ );
+ }
+ if (projectId in archiveYoung.timeTracking.project) {
+ delete archiveYoung.timeTracking.project[projectId];
+ await this._pfapiService.m.archiveYoung.save(archiveYoung, {
+ isUpdateRevAndLastUpdate: true,
+ });
+ this._archiveYoungUpdateTrigger$.next();
+ }
+
+ if (projectId in archiveOld.timeTracking.project) {
+ delete archiveOld.timeTracking.project[projectId];
+ await this._pfapiService.m.archiveOld.save(archiveOld, {
+ isUpdateRevAndLastUpdate: true,
+ });
+ this._archiveOldUpdateTrigger$.next();
+ }
+ }
+
+ async cleanupDataEverywhereForTag(tagId: string): Promise {
+ const current = await this.current$.pipe(first()).toPromise();
+ const archiveYoung = await this._pfapiService.m.archiveYoung.load();
+ const archiveOld = await this._pfapiService.m.archiveOld.load();
+
+ if (tagId in current.tag) {
+ const newTag = { ...current.tag };
+ delete newTag[tagId];
+ this._store.dispatch(
+ TimeTrackingActions.updateWholeState({
+ newState: {
+ ...current,
+ tag: { ...newTag },
+ },
+ }),
+ );
+ }
+
+ if (tagId in archiveYoung.timeTracking.tag) {
+ delete archiveYoung.timeTracking.tag[tagId];
+ await this._pfapiService.m.archiveYoung.save(archiveYoung, {
+ isUpdateRevAndLastUpdate: true,
+ });
+ this._archiveYoungUpdateTrigger$.next();
+ }
+
+ if (tagId in archiveOld.timeTracking.tag) {
+ delete archiveOld.timeTracking.tag[tagId];
+ await this._pfapiService.m.archiveOld.save(archiveOld, {
+ isUpdateRevAndLastUpdate: true,
+ });
+ this._archiveOldUpdateTrigger$.next();
+ }
+ }
+
+ async getWorkStartEndForWorkContext(ctx: {
+ id: string;
+ type: WorkContextType;
+ }): Promise> {
+ return this.getWorkStartEndForWorkContext$(ctx).pipe(first()).toPromise();
+ }
+
+ async getLegacyWorkStartEndForWorkContext(ctx: {
+ id: string;
+ type: WorkContextType;
+ }): Promise<{
+ workStart: WorkStartEnd;
+ workEnd: WorkStartEnd;
+ }> {
+ const d = await this.getWorkStartEndForWorkContext(ctx);
+ return toLegacyWorkStartEndMaps(d);
+ }
+}
diff --git a/src/app/features/time-tracking/to-legacy-work-start-end-maps.ts b/src/app/features/time-tracking/to-legacy-work-start-end-maps.ts
new file mode 100644
index 00000000000..10ce6c70afa
--- /dev/null
+++ b/src/app/features/time-tracking/to-legacy-work-start-end-maps.ts
@@ -0,0 +1,24 @@
+import { TTWorkSessionByDateMap } from './time-tracking.model';
+import { WorkStartEnd, WorkStartEndCopy } from '../work-context/work-context.model';
+
+// TODO maybe replace with using the data differently
+export const toLegacyWorkStartEndMaps = (
+ byDate: TTWorkSessionByDateMap,
+): {
+ workStart: WorkStartEnd;
+ workEnd: WorkStartEnd;
+} => {
+ const workStart: WorkStartEndCopy = {};
+ const workEnd: WorkStartEndCopy = {};
+
+ Object.entries(byDate).forEach(([date, d]) => {
+ if (d.s) {
+ workStart[date] = d.s;
+ }
+ if (d.e) {
+ workEnd[date] = d.e;
+ }
+ });
+
+ return { workStart, workEnd };
+};
diff --git a/src/app/features/work-context/store/work-context.selectors.spec.ts b/src/app/features/work-context/store/work-context.selectors.spec.ts
index 4cd7e4ea677..2795e772bff 100644
--- a/src/app/features/work-context/store/work-context.selectors.spec.ts
+++ b/src/app/features/work-context/store/work-context.selectors.spec.ts
@@ -34,8 +34,8 @@ describe('workContext selectors', () => {
separateTasksBy: ' | ',
},
},
- breakNr: {},
- breakTime: {},
+ // breakNr: {},
+ // breakTime: {},
color: null,
// created: 1620997370531,
icon: 'wb_sunny',
@@ -46,8 +46,8 @@ describe('workContext selectors', () => {
theme: TODAY_TAG.theme,
title: 'Today',
type: 'TAG',
- workEnd: {},
- workStart: {},
+ // workEnd: {},
+ // workStart: {},
}),
);
});
diff --git a/src/app/features/work-context/work-context.const.ts b/src/app/features/work-context/work-context.const.ts
index 6934aab4774..4c641100eda 100644
--- a/src/app/features/work-context/work-context.const.ts
+++ b/src/app/features/work-context/work-context.const.ts
@@ -35,10 +35,8 @@ export const WORK_CONTEXT_DEFAULT_COMMON: WorkContextCommon = {
worklogExportSettings: WORKLOG_EXPORT_DEFAULTS,
},
theme: WORK_CONTEXT_DEFAULT_THEME,
- workStart: {},
- workEnd: {},
- breakTime: {},
- breakNr: {},
+ // breakTime: {},
+ // breakNr: {},
taskIds: [],
icon: null,
id: '',
diff --git a/src/app/features/work-context/work-context.model.ts b/src/app/features/work-context/work-context.model.ts
index 0bdad52510b..2883d565600 100644
--- a/src/app/features/work-context/work-context.model.ts
+++ b/src/app/features/work-context/work-context.model.ts
@@ -18,6 +18,7 @@ type HueValue =
| 'A400'
| 'A700';
+// TODO REMOVE OR MOVE AND LEGACY RENAME ALL THESE
export interface BreakTimeCopy {
[key: string]: number;
}
@@ -40,17 +41,18 @@ export type WorkContextAdvancedCfg = Readonly<{
worklogExportSettings: WorklogExportSettings;
}>;
+// TODO handle more strictly
export type WorkContextThemeCfg = Readonly<{
- isAutoContrast: boolean;
- isDisableBackgroundGradient: boolean;
- primary: string;
- huePrimary: HueValue;
- accent: string;
- hueAccent: HueValue;
- warn: string;
- hueWarn: HueValue;
- backgroundImageDark: string | null;
- backgroundImageLight: string | null;
+ isAutoContrast?: boolean;
+ isDisableBackgroundGradient?: boolean;
+ primary?: string;
+ huePrimary?: HueValue;
+ accent?: string;
+ hueAccent?: HueValue;
+ warn?: string;
+ hueWarn?: HueValue;
+ backgroundImageDark?: string | null;
+ backgroundImageLight?: string | null;
}>;
export enum WorkContextType {
@@ -59,16 +61,18 @@ export enum WorkContextType {
}
export interface WorkContextCommon {
- workStart: WorkStartEnd;
- workEnd: WorkStartEnd;
- breakTime: BreakTime;
- breakNr: BreakNr;
advancedCfg: WorkContextAdvancedCfg;
theme: WorkContextThemeCfg;
icon: string | null;
taskIds: string[];
id: string;
title: string;
+
+ // TODO remove legacy
+ breakTime?: BreakTime;
+ breakNr?: BreakNr;
+ workStart?: WorkStartEnd;
+ workEnd?: WorkStartEnd;
}
export type WorkContextAdvancedCfgKey = keyof WorkContextAdvancedCfg;
diff --git a/src/app/features/work-context/work-context.service.ts b/src/app/features/work-context/work-context.service.ts
index 77d5ded5b81..b3546b18da1 100644
--- a/src/app/features/work-context/work-context.service.ts
+++ b/src/app/features/work-context/work-context.service.ts
@@ -1,4 +1,4 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { combineLatest, interval, Observable, of, timer } from 'rxjs';
import {
@@ -36,18 +36,8 @@ import {
} from '../tasks/store/task.selectors';
import { Actions, ofType } from '@ngrx/effects';
import { WorklogExportSettings } from '../worklog/worklog.model';
-import {
- addToProjectBreakTime,
- updateProjectAdvancedCfg,
- updateProjectWorkEnd,
- updateProjectWorkStart,
-} from '../project/store/project.actions';
-import {
- addToBreakTimeForTag,
- updateAdvancedConfigForTag,
- updateWorkEndForTag,
- updateWorkStartForTag,
-} from '../tag/store/tag.actions';
+import { updateProjectAdvancedCfg } from '../project/store/project.actions';
+import { updateAdvancedConfigForTag } from '../tag/store/tag.actions';
import { allDataWasLoaded } from '../../root-store/meta/all-data-was-loaded.actions';
import {
selectActiveContextId,
@@ -68,7 +58,9 @@ import { isShallowEqual } from '../../util/is-shallow-equal';
import { distinctUntilChangedObject } from '../../util/distinct-until-changed-object';
import { DateService } from 'src/app/core/date/date.service';
import { getTimeSpentForDay } from './get-time-spent-for-day.util';
-import { PersistenceService } from '../../core/persistence/persistence.service';
+import { TimeTrackingService } from '../time-tracking/time-tracking.service';
+import { TimeTrackingActions } from '../time-tracking/store/time-tracking.actions';
+import { TaskArchiveService } from '../time-tracking/task-archive.service';
@Injectable({
providedIn: 'root',
@@ -81,7 +73,8 @@ export class WorkContextService {
private _dateService = inject(DateService);
private _router = inject(Router);
private _translateService = inject(TranslateService);
- private _persistenceService = inject(PersistenceService);
+ private _timeTrackingService = inject(TimeTrackingService);
+ private _taskArchiveService = inject(TaskArchiveService);
// here because to avoid circular dependencies
// should be treated as private
@@ -143,6 +136,10 @@ export class WorkContextService {
switchMap(() => this._store$.select(selectActiveWorkContext)),
shareReplay(1),
);
+ activeWorkContextTTData$ = this.activeWorkContext$.pipe(
+ switchMap((ac) => this._timeTrackingService.getWorkStartEndForWorkContext$(ac)),
+ shareReplay(1),
+ );
activeWorkContextTitle$: Observable = this.activeWorkContext$.pipe(
map((activeContext) => activeContext.title),
@@ -389,7 +386,7 @@ export class WorkContextService {
const { activeId, activeType } = await this.activeWorkContextTypeAndId$
.pipe(first())
.toPromise();
- const taskArchiveState = await this._persistenceService.taskArchive.loadState();
+ const taskArchiveState = await this._taskArchiveService.load();
const { ids, entities } = taskArchiveState;
const tasksWorkedOnToday: ArchiveTask[] = ids
@@ -434,26 +431,35 @@ export class WorkContextService {
);
}
- getWorkStart$(day: string = this._dateService.todayStr()): Observable {
- return this.activeWorkContext$.pipe(map((ctx) => ctx.workStart[day]));
+ // TODO merge this stuff
+ getWorkStart$(
+ day: string = this._dateService.todayStr(),
+ ): Observable {
+ return this.activeWorkContextTTData$.pipe(map((byDateMap) => byDateMap[day]?.s));
}
- getWorkEnd$(day: string = this._dateService.todayStr()): Observable {
- return this.activeWorkContext$.pipe(map((ctx) => ctx.workEnd[day]));
+ getWorkEnd$(
+ day: string = this._dateService.todayStr(),
+ ): Observable {
+ return this.activeWorkContextTTData$.pipe(map((byDateMap) => byDateMap[day]?.e));
}
- getBreakTime$(day: string = this._dateService.todayStr()): Observable {
- return this.activeWorkContext$.pipe(map((ctx) => ctx.breakTime[day]));
+ getBreakTime$(
+ day: string = this._dateService.todayStr(),
+ ): Observable {
+ return this.activeWorkContextTTData$.pipe(map((byDateMap) => byDateMap[day]?.bt));
}
- getBreakNr$(day: string = this._dateService.todayStr()): Observable {
- return this.activeWorkContext$.pipe(map((ctx) => ctx.breakNr[day]));
+ getBreakNr$(
+ day: string = this._dateService.todayStr(),
+ ): Observable {
+ return this.activeWorkContextTTData$.pipe(map((byDateMap) => byDateMap[day]?.b));
}
async load(): Promise {
// NOTE: currently route has prevalence over everything else and as there is not state apart from
// activeContextId, and activeContextType, we don't need to load it
- // const state = await this._persistenceService.context.loadState() || initialContextState;
+ // const state = await this._pfapiService.context.loadState() || initialContextState;
// this._store$.dispatch(loadWorkContextState({state}));
}
@@ -464,45 +470,48 @@ export class WorkContextService {
}
updateWorkStartForActiveContext(date: string, newVal: number): void {
- const payload: { id: string; date: string; newVal: number } = {
- id: this.activeWorkContextId as string,
- date,
- newVal,
- };
- const action =
- this.activeWorkContextType === WorkContextType.PROJECT
- ? updateProjectWorkStart(payload)
- : updateWorkStartForTag(payload);
- this._store$.dispatch(action);
+ if (!this.activeWorkContextId || !this.activeWorkContextType) {
+ throw new Error('Invalid active work context');
+ }
+ this._store$.dispatch(
+ TimeTrackingActions.updateWorkContextData({
+ ctx: { id: this.activeWorkContextId, type: this.activeWorkContextType },
+ date,
+ updates: { s: newVal },
+ }),
+ );
}
updateWorkEndForActiveContext(date: string, newVal: number): void {
- const payload: { id: string; date: string; newVal: number } = {
- id: this.activeWorkContextId as string,
- date,
- newVal,
- };
- const action =
- this.activeWorkContextType === WorkContextType.PROJECT
- ? updateProjectWorkEnd(payload)
- : updateWorkEndForTag(payload);
- this._store$.dispatch(action);
+ if (!this.activeWorkContextId || !this.activeWorkContextType) {
+ throw new Error('Invalid active work context');
+ }
+ this._store$.dispatch(
+ TimeTrackingActions.updateWorkContextData({
+ ctx: { id: this.activeWorkContextId, type: this.activeWorkContextType },
+ date,
+ updates: { e: newVal },
+ }),
+ );
}
- addToBreakTimeForActiveContext(
+ async addToBreakTimeForActiveContext(
date: string = this._dateService.todayStr(),
valToAdd: number,
- ): void {
- const payload: { id: string; date: string; valToAdd: number } = {
- id: this.activeWorkContextId as string,
- date,
- valToAdd,
- };
- const action =
- this.activeWorkContextType === WorkContextType.PROJECT
- ? addToProjectBreakTime(payload)
- : addToBreakTimeForTag(payload);
- this._store$.dispatch(action);
+ ): Promise {
+ if (!this.activeWorkContextId || !this.activeWorkContextType) {
+ throw new Error('Invalid active work context');
+ }
+ const currentBreakTime = (await this.getBreakTime$().pipe(first()).toPromise()) || 0;
+ const currentBreakNr = (await this.getBreakNr$().pipe(first()).toPromise()) || 0;
+
+ this._store$.dispatch(
+ TimeTrackingActions.updateWorkContextData({
+ ctx: { id: this.activeWorkContextId, type: this.activeWorkContextType },
+ date,
+ updates: { b: currentBreakNr + 1, bt: currentBreakTime + valToAdd },
+ }),
+ );
}
private _updateAdvancedCfgForCurrentContext(
diff --git a/src/app/features/worklog/worklog-export/worklog-export.component.ts b/src/app/features/worklog/worklog-export/worklog-export.component.ts
index 4d62f42115e..439f97585ad 100644
--- a/src/app/features/worklog/worklog-export/worklog-export.component.ts
+++ b/src/app/features/worklog/worklog-export/worklog-export.component.ts
@@ -8,7 +8,7 @@ import {
OnInit,
output,
} from '@angular/core';
-import { combineLatest, Subscription } from 'rxjs';
+import { combineLatest, from, Subscription } from 'rxjs';
import { getWorklogStr } from '../../../util/get-work-log-str';
import 'moment-duration-format';
// @ts-ignore
@@ -21,7 +21,7 @@ import {
WorklogGrouping,
} from '../worklog.model';
import { T } from '../../../t.const';
-import { distinctUntilChanged } from 'rxjs/operators';
+import { distinctUntilChanged, switchMap } from 'rxjs/operators';
import { distinctUntilChangedObject } from '../../../util/distinct-until-changed-object';
import { WorkContextAdvancedCfg } from '../../work-context/work-context.model';
import { WORKLOG_EXPORT_DEFAULTS } from '../../work-context/work-context.const';
@@ -44,6 +44,7 @@ import { MatFormField } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { SimpleDownloadDirective } from '../../../ui/simple-download/simple-download.directive';
import { TranslatePipe } from '@ngx-translate/core';
+import { TimeTrackingService } from '../../time-tracking/time-tracking.service';
@Component({
selector: 'worklog-export',
@@ -80,6 +81,7 @@ export class WorklogExportComponent implements OnInit, OnDestroy {
private _changeDetectorRef = inject(ChangeDetectorRef);
private _projectService = inject(ProjectService);
private _tagService = inject(TagService);
+ private _timeTrackingService = inject(TimeTrackingService);
readonly rangeStart = input();
readonly rangeEnd = input();
@@ -177,14 +179,21 @@ export class WorklogExportComponent implements OnInit, OnDestroy {
true,
this.projectId(),
),
- this._workContextService.activeWorkContext$,
this._projectService.list$,
this._tagService.tags$,
+ this._workContextService.activeWorkContext$.pipe(
+ switchMap((ac) =>
+ from(this._timeTrackingService.getLegacyWorkStartEndForWorkContext(ac)),
+ ),
+ ),
])
.pipe()
- .subscribe(([tasks, ac, projects, tags]) => {
+ .subscribe(([tasks, projects, tags, activeContextTimeTracking]) => {
if (tasks) {
- const workTimes = { start: ac.workStart, end: ac.workEnd };
+ const workTimes = {
+ start: activeContextTimeTracking.workStart,
+ end: activeContextTimeTracking.workEnd,
+ };
const data = { tasks, projects, tags, workTimes };
const rows = createRows(data, this.options.groupBy);
this.formattedRows = formatRows(rows, this.options);
diff --git a/src/app/features/worklog/worklog.component.ts b/src/app/features/worklog/worklog.component.ts
index fd335ccb3b5..971b938da56 100644
--- a/src/app/features/worklog/worklog.component.ts
+++ b/src/app/features/worklog/worklog.component.ts
@@ -5,7 +5,6 @@ import {
inject,
OnDestroy,
} from '@angular/core';
-import { PersistenceService } from '../../core/persistence/persistence.service';
import { expandFadeAnimation } from '../../ui/animations/expand.ani';
import { WorklogDataForDay, WorklogMonth, WorklogWeek } from './worklog.model';
import { MatDialog } from '@angular/material/dialog';
@@ -35,6 +34,8 @@ import { MsToClockStringPipe } from '../../ui/duration/ms-to-clock-string.pipe';
import { MsToStringPipe } from '../../ui/duration/ms-to-string.pipe';
import { NumberToMonthPipe } from '../../ui/pipes/number-to-month.pipe';
import { TranslatePipe } from '@ngx-translate/core';
+import { PfapiService } from '../../pfapi/pfapi.service';
+import { TaskArchiveService } from '../time-tracking/task-archive.service';
@Component({
selector: 'worklog',
@@ -67,12 +68,13 @@ import { TranslatePipe } from '@ngx-translate/core';
export class WorklogComponent implements AfterViewInit, OnDestroy {
readonly worklogService = inject(WorklogService);
readonly workContextService = inject(WorkContextService);
- private readonly _persistenceService = inject(PersistenceService);
+ private readonly _pfapiService = inject(PfapiService);
private readonly _taskService = inject(TaskService);
private readonly _matDialog = inject(MatDialog);
private readonly _router = inject(Router);
private readonly _route = inject(ActivatedRoute);
private readonly _store = inject(Store);
+ private readonly _taskArchiveService = inject(TaskArchiveService);
T: typeof T = T;
expanded: { [key: string]: boolean } = {};
@@ -133,7 +135,7 @@ export class WorklogComponent implements AfterViewInit, OnDestroy {
if (isConfirm) {
let subTasks;
if (task.subTaskIds && task.subTaskIds.length) {
- const archiveState = await this._persistenceService.taskArchive.loadState();
+ const archiveState = await this._taskArchiveService.load();
subTasks = task.subTaskIds
.map((id) => archiveState.entities[id])
.filter((v) => !!v);
diff --git a/src/app/features/worklog/worklog.service.ts b/src/app/features/worklog/worklog.service.ts
index fad7f672da6..f093eb6e237 100644
--- a/src/app/features/worklog/worklog.service.ts
+++ b/src/app/features/worklog/worklog.service.ts
@@ -1,4 +1,4 @@
-import { Injectable, inject } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import {
Worklog,
WorklogDay,
@@ -7,7 +7,6 @@ import {
WorklogYearsWithWeeks,
} from './worklog.model';
import { dedupeByKey } from '../../util/de-dupe-by-key';
-import { PersistenceService } from '../../core/persistence/persistence.service';
import { BehaviorSubject, from, merge, Observable } from 'rxjs';
import {
concatMap,
@@ -27,27 +26,32 @@ import { TaskService } from '../tasks/task.service';
import { createEmptyEntity } from '../../util/create-empty-entity';
import { getCompleteStateForWorkContext } from './util/get-complete-state-for-work-context.util';
import { NavigationEnd, Router } from '@angular/router';
-import { DataInitService } from '../../core/data-init/data-init.service';
import { WorklogTask } from '../tasks/task.model';
import { mapArchiveToWorklogWeeks } from './util/map-archive-to-worklog-weeks';
import moment from 'moment';
import { DateAdapter } from '@angular/material/core';
+import { PfapiService } from '../../pfapi/pfapi.service';
+import { DataInitStateService } from '../../core/data-init/data-init-state.service';
+import { TimeTrackingService } from '../time-tracking/time-tracking.service';
+import { TaskArchiveService } from '../time-tracking/task-archive.service';
@Injectable({ providedIn: 'root' })
export class WorklogService {
- private readonly _persistenceService = inject(PersistenceService);
+ private readonly _pfapiService = inject(PfapiService);
private readonly _workContextService = inject(WorkContextService);
- private readonly _dataInitService = inject(DataInitService);
+ private readonly _dataInitStateService = inject(DataInitStateService);
private readonly _taskService = inject(TaskService);
+ private readonly _timeTrackingService = inject(TimeTrackingService);
private readonly _router = inject(Router);
private _dateAdapter = inject>(DateAdapter);
+ private _taskArchiveService = inject(TaskArchiveService);
// treated as private but needs to be assigned first
archiveUpdateManualTrigger$: BehaviorSubject = new BehaviorSubject(
true,
);
_archiveUpdateTrigger$: Observable =
- this._dataInitService.isAllDataLoadedInitially$.pipe(
+ this._dataInitStateService.isAllDataLoadedInitially$.pipe(
concatMap(() =>
merge(
// this._workContextService.activeWorkContextOnceOnContextChange$,
@@ -210,8 +214,7 @@ export class WorklogService {
private async _loadWorklogForWorkContext(
workContext: WorkContext,
): Promise<{ worklog: Worklog; totalTimeSpent: number }> {
- const archive =
- (await this._persistenceService.taskArchive.loadState()) || createEmptyEntity();
+ const archive = (await this._taskArchiveService.load()) || createEmptyEntity();
const taskState =
(await this._taskService.taskFeatureState$.pipe(first()).toPromise()) ||
createEmptyEntity();
@@ -221,16 +224,14 @@ export class WorklogService {
getCompleteStateForWorkContext(workContext, taskState, archive);
// console.timeEnd('calcTime');
- const startEnd = {
- workStart: workContext.workStart,
- workEnd: workContext.workEnd,
- };
+ const workStartEndForWorkContext =
+ await this._timeTrackingService.getLegacyWorkStartEndForWorkContext(workContext);
if (completeStateForWorkContext) {
const { worklog, totalTimeSpent } = mapArchiveToWorklog(
completeStateForWorkContext,
nonArchiveTaskIds,
- startEnd,
+ workStartEndForWorkContext,
this._dateAdapter.getFirstDayOfWeek(),
);
return {
@@ -247,8 +248,7 @@ export class WorklogService {
private async _loadQuickHistoryForWorkContext(
workContext: WorkContext,
): Promise {
- const archive =
- (await this._persistenceService.taskArchive.loadState()) || createEmptyEntity();
+ const archive = (await this._taskArchiveService.load()) || createEmptyEntity();
const taskState =
(await this._taskService.taskFeatureState$.pipe(first()).toPromise()) ||
createEmptyEntity();
@@ -258,16 +258,14 @@ export class WorklogService {
getCompleteStateForWorkContext(workContext, taskState, archive);
// console.timeEnd('calcTime');
- const startEnd = {
- workStart: workContext.workStart,
- workEnd: workContext.workEnd,
- };
+ const workStartEndForWorkContext =
+ await this._timeTrackingService.getLegacyWorkStartEndForWorkContext(workContext);
if (completeStateForWorkContext) {
return mapArchiveToWorklogWeeks(
completeStateForWorkContext,
nonArchiveTaskIds,
- startEnd,
+ workStartEndForWorkContext,
this._dateAdapter.getFirstDayOfWeek(),
);
}
diff --git a/src/app/imex/file-imex/file-imex.component.ts b/src/app/imex/file-imex/file-imex.component.ts
index d6d6e1687ee..3a4058b3944 100644
--- a/src/app/imex/file-imex/file-imex.component.ts
+++ b/src/app/imex/file-imex/file-imex.component.ts
@@ -5,9 +5,7 @@ import {
inject,
viewChild,
} from '@angular/core';
-import { DataImportService } from '../sync/data-import.service';
import { SnackService } from '../../core/snack/snack.service';
-import { AppDataComplete } from '../sync/sync.model';
import { download } from '../../util/download';
import { T } from '../../t.const';
import { TODAY_TAG } from '../../features/tag/tag.const';
@@ -17,6 +15,8 @@ import { MatIcon } from '@angular/material/icon';
import { MatButton } from '@angular/material/button';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslatePipe } from '@ngx-translate/core';
+import { AppDataCompleteNew } from '../../pfapi/pfapi-config';
+import { PfapiService } from 'src/app/pfapi/pfapi.service';
@Component({
selector: 'file-imex',
@@ -26,9 +26,9 @@ import { TranslatePipe } from '@ngx-translate/core';
imports: [MatIcon, MatButton, MatTooltip, TranslatePipe],
})
export class FileImexComponent {
- private _dataImportService = inject(DataImportService);
private _snackService = inject(SnackService);
private _router = inject(Router);
+ private _pfapiService = inject(PfapiService);
readonly fileInputRef = viewChild('fileInput');
T: typeof T = T;
@@ -41,7 +41,7 @@ export class FileImexComponent {
reader.onload = async () => {
const textData = reader.result;
console.log(textData);
- let data: AppDataComplete | undefined;
+ let data: AppDataCompleteNew | undefined;
let oldData;
try {
data = oldData = JSON.parse((textData as any).toString());
@@ -55,7 +55,15 @@ export class FileImexComponent {
alert('V1 Data. Migration not supported any more.');
} else {
await this._router.navigate([`tag/${TODAY_TAG.id}/tasks`]);
- await this._dataImportService.importCompleteSyncData(data as AppDataComplete);
+ try {
+ await this._pfapiService.importCompleteBackup(data as AppDataCompleteNew);
+ } catch (e) {
+ this._snackService.open({
+ type: 'ERROR',
+ msg: T.FILE_IMEX.S_ERR_IMPORT_FAILED,
+ });
+ return;
+ }
}
const fileInputRef = this.fileInputRef();
@@ -72,13 +80,13 @@ export class FileImexComponent {
}
async downloadBackup(): Promise